Home / Alt manpages / test(1)

  • test(1)
  • User command
  • linux

Use test Safely in Shell Conditions

An if statement that silently does the wrong thing is almost always test being fed an unquoted variable or the wrong operator. This guide covers file, string and integer checks, the traps that make scripts hard to trust, and how to tell a true condition from a failed command. Examples match GNU coreutils 9.4, installed here as coreutils package 9.4-3ubuntu6.3.

Allow about fifteen minutes. You need a POSIX-style shell and a writable working directory for the temporary examples. Every check here only inspects values and file metadata: nothing needs sudo, and none of it changes a file or service.

1. Check which test you are running

Most shells provide test and [ as builtins. A builtin normally runs before the external GNU program, so the manpage describes the expression language, but your shell decides the actual implementation:

$ type -a test
test is a shell builtin
test is /usr/bin/test
$ dpkg-query -W -f='${Package} ${Version}\n' coreutils
coreutils 9.4-3ubuntu6.3

Checkpoint

The command you use in a script is probably the shell builtin. If you need the external GNU implementation specifically, call /usr/bin/test. The same split applies to [.

2. Test whether a path exists

Use -e for existence, -f when you need a regular file specifically. Quote the path so whitespace and wildcard characters stay part of the path, not the shell's business:

if test -f -- /etc/passwd; then
    printf '%s\n' 'The regular file exists'
else
    printf '%s\n' 'The regular file is missing or not regular' >&2
    exit 1
fi

if [ -d -- /etc ]; then
    printf '%s\n' '/etc is a directory'
fi

With the bracket spelling, the closing ] is a separate argument, syntax rather than decoration. -- separates options from the path where supported; for a path that genuinely starts with a hyphen, something like ./-name sidesteps the ambiguity.

Existence is not usability. -r, -w and -x check access, -s checks the file is bigger than zero bytes. These answer for right now, not a guarantee that a later operation cannot still fail.

Checkpoint

Check the status directly, no message required:

$ test -e /etc/passwd
$ printf 'status: %s\n' "$?"
status: 0
$ test -e /path/that/does/not/exist
$ printf 'status: %s\n' "$?"
status: 1

3. Compare strings without relying on unset values

Use = and != for string equality, -n and -z for non-empty and empty. Quote every variable expansion:

expected='production'
actual='production'

if [ "$actual" = "$expected" ]; then
    printf '%s\n' 'The values match'
fi

if [ -z "${OPTIONAL_VALUE:-}" ]; then
    printf '%s\n' 'OPTIONAL_VALUE is empty or unset'
fi

test STRING means the same as test -n STRING, but an unquoted empty expansion can vanish an argument entirely and reshape the whole expression. Quoting is the one habit that heads this off. ${OPTIONAL_VALUE:-} supplies an empty value for an unset variable without changing it.

Warning

Never treat arbitrary input as a complete expression. A value starting with a test operator can be read as syntax, and an unquoted value with spaces can split into several arguments. Keep data in quoted arguments and keep the operators in the script.

4. Compare integers with integer operators

For numbers use -eq, -ne, -lt, -le, -gt and -ge. = is a string comparison, not a numeric one, no matter how tempting it looks:

attempts=3
limit=5

if [ "$attempts" -lt "$limit" ]; then
    printf 'retry allowed: %s is below %s\n' "$attempts" "$limit"
fi

Both operands need to be valid integers for your shell. Validate or constrain input before it reaches a numeric comparison. GNU test also accepts -l STRING for string length, though ${#value} is usually easier to read and is not part of this command's portable vocabulary.

5. Combine checks with shell control operators

For two conditions, reach for shell control operators. They short-circuit and make the grouping visible at a glance:

if test -f /etc/passwd && test -r /etc/passwd; then
    printf '%s\n' 'A readable regular file is present'
fi

if test -z "${CONFIG_FILE:-}" || test ! -f "$CONFIG_FILE"; then
    printf '%s\n' 'No usable configuration path was supplied' >&2
fi

The manual documents binary -a and -o but warns they are inherently ambiguous. Use separate test commands joined with && or || instead, so the shell decides the control flow rather than an overloaded expression parser guessing how operands group.

Parentheses can group a test expression, but the shell sees them as syntax first, so they need escaping: \( ... \). In ordinary scripts, separate commands are usually clearer and dodge yet another quoting headache.

6. Remember what the status means

test prints nothing for a normal check. Status 0 means true, status 1 means false, anything larger usually means a malformed expression or another error. Capture the status immediately if you need to diagnose it:

$ test 1 -lt 2
$ result=$?
$ printf 'true=%s\n' "$result"
true=0
$ test 1 -lt
$ result=$?
$ printf 'invalid-expression=%s\n' "$result"
invalid-expression=2

Do not write test ...; echo "$?"; test ... and assume the first result is still sitting there. Every command overwrites $?. Put the test directly in the if condition so the shell branches on the command you actually mean.

7. Avoid the option trap

The manpage says [ honours --help and --version, while test treats those words as ordinary non-empty strings. Your shell builtin may do something different again:

$ /usr/bin/[ --version
[ (GNU coreutils) 9.4
$ /usr/bin/test --version
$ printf 'test status: %s\n' "$?"
test status: 0

Use command -V test or type -a test to see what your shell actually picks. Call /usr/bin/[ --version only when you specifically need the external GNU bracket program. Never use test --version as a portable version probe: it just returns true because the string is non-empty.

File tests follow symbolic links except for -h and -L, which check the link itself rather than its target. That difference matters when checking a deployment path: -f asks whether the target resolves to a regular file, -L asks whether the link exists at all.

Done means

  • Identified your test. You know whether your shell picked a builtin or /usr/bin/test.
  • Quoted and matched operators. File, string and integer checks all use quoted values and the right operator.
  • Combined checks properly. && and || replace ambiguous -a and -o.
  • Read status correctly. Status 0 is true, and you capture $? before running anything else.
  • Skipped the version trap. You know test --version is not a reliable check.