Where to start First steps track · 15 stages
Linux Labs

test, [ ] and [[ ]] — checking conditions

7 min Ubuntu 24.04 · bash 5.2 · dash 0.5

test and [ ] check a file, a string or a number and answer with a code: 0 is yes, 1 is no; [[ ]] is the bash version.

Cheat sheet

8 commands Printable version
  • [ -f /etc/app.conf ] && echo "file exists"
  • [ -d "$dir" ] || mkdir -p "$dir"
  • [ -z "$1" ] && { echo "Usage: $0 FILE" >&2; exit 2; }
  • [ "$answer" = yes ] strings are equal
  • [ "$count" -gt 10 ] number greater than 10
  • [ -r "$f" ] && [ -s "$f" ] readable and not empty
  • [[ $name == *.log ]] pattern, bash only
  • [[ $ver =~ ^[0-9]+\.[0-9]+$ ]] regular expression, bash only

Overview

test checks a condition and prints nothing — it answers with an exit code: 0 if the condition holds, 1 if not, 2 if the expression itself is broken. [ is the same command under another name, and it expects ] as its last argument. That is why the spaces around the brackets matter: every part of the expression is a separate argument.

text
student@ubuntu:~$ type [ [[ test
[ is a shell builtin
[[ is a shell keyword
test is a shell builtin

[[ ]] is a bash keyword: the shell parses the expression itself, before variables are expanded. Inside [[ ]] an unquoted variable does not split into words, < and > are not redirections, and == matches a pattern. sh (dash) has no [[ ]], so a #!/bin/sh script sticks to [ ].

[ ] and test [[ ]]
works in any POSIX shell bash and zsh
unquoted $x splits into words — errors safe
pattern *.log no ==, !=
regular expression no =~
two conditions [ a ] && [ b ] [[ a && b ]]

There are three kinds of checks:

  • files: -e exists, -f regular file, -d directory, -L symbolic link, -r, -w, -x — the current user's permission, -s not empty, a -nt b — a is newer than b;
  • strings: = (bash also accepts ==), !=, -z empty, -n not empty;
  • numbers: -eq, -ne, -lt, -le, -gt, -ge.

A ! before a check inverts it: [ ! -f "$f" ] means there is no such file.

< and > compare strings character by character: [[ 10 > 9 ]] is false because "1" sorts before "9". Compare numbers with -gt, -lt or (( )).

Options

15

The test checks are in the table below. The full list is in help test; for [[ ]], see man bash, section CONDITIONAL EXPRESSIONS.

Option What it does Example
-e FILE The file exists (any type); a dangling link does not. [ -e /etc/app.conf ]
-f FILE A regular file — not a directory or a device. [ -f "$1" ] || exit 1
-d DIR A directory. [ -d /srv/backup ] || mkdir -p /srv/backup
-r / -w / -x FILE The current user may read, write, execute it. [ -w "$dir" ]
-z / -n STRING The string is empty / not empty; quote the variable. [ -z "$1" ]
= / != A B Strings are equal / differ; bash also accepts ==. [ "$answer" = yes ]
-eq / -ne A B Integers are equal / differ. [ $# -eq 1 ]
-lt / -le / -gt / -ge A B Less than, at most, greater than, at least. [ "$used" -ge 90 ]
-s FILE The file exists and is not empty. [ -s report.txt ]
-L FILE A symbolic link — even if its target is missing. [ -L /etc/localtime ]
! EXPR Invert the check. [ ! -f "$lock" ]
[[ == ]] PATTERN The string matches a pattern with * and ?; leave it unquoted. [[ $file == *.log ]]
[[ =~ ]] REGEX The string matches a regular expression; groups go to BASH_REMATCH. [[ $ver =~ ^[0-9]+\.[0-9]+$ ]]
-nt / -ot A B File A is newer (older) than B by modification time. [ app.conf -nt app.conf.bak ]
-a / -o "And", "or" inside [ ]; obsolete — write [ a ] && [ b ]. [ -d /tmp ] && [ -w /tmp ]

Nothing found. Try a shorter query.

Recipes

4

Validate a script argument

bash
if [ $# -ne 1 ]; then
  echo "Usage: $0 FILE" >&2
  exit 2
fi
if [ ! -r "$1" ]; then
  echo "cannot read: $1" >&2
  exit 1
fi

$# is the number of arguments. Keep variables in double quotes inside a check, so a file name with a space stays one argument.

Compare a number taken from command output

bash
used=$(df --output=pcent / | tail -1 | tr -dc '0-9')
if [ "$used" -ge 90 ]; then
  echo "root is $used% full" >&2
fi

-ge wants a bare number. tr -dc '0-9' strips the spaces and the % sign; without it the check fails with integer expression expected.

Patterns and regular expressions

bash
[[ $file == *.log ]] && echo "a log"
if [[ $day =~ ^([0-9]{4})-([0-9]{2}) ]]; then
  echo "year ${BASH_REMATCH[1]}, month ${BASH_REMATCH[2]}"
fi

With day=2026-10-20 the second condition prints year 2026, month 10: the groups in parentheses land in the BASH_REMATCH array. Leave the right-hand side unquoted: [[ app.log == "*.log" ]] compares literally and returns 1.

Several conditions at once

bash
[ -d "$dir" ] && [ -w "$dir" ]     # and
[ "$x" = a ] || [ "$x" = b ]       # or
[[ -d $dir && -w $dir ]]           # the same in bash

Troubleshooting

8

[: =: unary operator expected

The variable is empty and unquoted: [ $x = foo ] became [ = foo ]. Write [ "$x" = foo ].

[: too many arguments

The value contains a space: with x="a b" the check [ $x = foo ] received a, b, =, foo. Quote the variable or switch to [[ ]].

[: a: integer expression expected

-eq, -gt and the rest got something that is not a number: an empty string, 85%, a word. Print the value (echo "[$used]") and strip it down to digits.

[-f: command not found and missing `]'

A space is missing after [ or before ]. The shell looks up [-f as a command name, and [ -f file] never sees its closing ] argument.

[: a: unexpected operator

This is how sh (dash) reacts to == inside [ ]. POSIX compares strings with =. Either write =, or run the script with bash: #!/bin/bash on the first line.

[ -n $x ] is true for an empty variable

Unquoted, the empty value disappears and [ -n ] is left — a check that the string -n is not empty, which is true. Quoted, [ -n "$x" ] returns 1 as you expect.

value too great for base

[[ 08 -eq 8 ]] and (( 08 )) read a number with a leading zero as octal, and octal has no 8 or 9. Months and days from date +%m break this way. Write 10#$m, or compare inside [ ], where 08 is plain decimal.

-e and -f look at the file the link points to. If the target is missing, the check is false. -L checks the link itself.

Danger zone

2

[ 10 > 9 ] creates a file

Inside [ ] the > sign is a redirection. The shell creates an empty file named 9 in the current directory, and the check shrinks to [ 10 ], which is always true — even [ 1 > 9 ]:

text
student@ubuntu:/tmp$ [ 1 > 9 ]; echo $?
0
student@ubuntu:/tmp$ ls -l 9
-rw-rw-r-- 1 student student 0 Oct 20 10:01 9

Compare numbers with -gt.

The check and the action are two commands

Between [ -f "$lock" ] and creating the file, another copy of the script can do the same. Guarding against a second run needs a lock — flock.

See also

  • exit-codes — codes 0, 1, 2 and the &&, || operators.
  • bash-scripting — if, loops and script arguments.
  • command-syntax — quoting and how a line splits into arguments.
  • globbing — the * and ? patterns, the same ones [[ == ]] uses.
  • regex — expression syntax for =~.
  • permissions — what r, w, x mean for -r, -w, -x.
  • find — checks across many files at once.
  • shellcheck — catches unquoted variables inside [ ].
Loading…