Use test Safely in Shell Conditions
You will finish with shell checks that distinguish a true condition from a failed command, cover the common file, string and integer cases, and avoid the expression traps that make test scripts hard to trust. The examples match GNU coreutils 9.4, installed here as coreutils package version 9.4-3ubuntu6.3.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a POSIX-style shell and a writable working directory for the temporary examples. These checks only inspect values and file metadata. They do not need sudo, and none of the examples 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 installed manpage describes the expression language but your shell still controls the exact 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 may therefore be the shell builtin. If you need to inspect the external GNU implementation specifically, call /usr/bin/test. The same distinction applies to [.
2. Test whether a path exists
Use -e for existence and -f when you require a regular file. Put the path in quotes so whitespace and wildcard characters remain part of the path:
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. It is syntax, not a decorative character. The -- separates options from the path for implementations that support it; for a path beginning with a hyphen, use a path such as ./-name when practical.
Do not confuse existence with usability. The file tests include -r, -w and -x for access checks, while -s checks that a file is larger than zero bytes. These are answers about the current process and filesystem, not guarantees that a later operation cannot fail.
Checkpoint: verify the status directly without printing a message:
$ 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, or -n and -z for non-empty and empty strings. Quote variable expansions:
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
The form test STRING means the same as test -n STRING, but an unquoted empty expansion can remove an argument and change the expression shape. Quoting is the small habit that prevents this class of bug. The ${OPTIONAL_VALUE:-} expansion supplies an empty value when the variable is unset, without modifying the variable.
Never treat arbitrary input as a complete expression. A value beginning with a test operator can be interpreted as syntax, and an unquoted value containing 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. Do not use = when you mean numeric equality:
attempts=3
limit=5
if [ "$attempts" -lt "$limit" ]; then
printf 'retry allowed: %s is below %s\n' "$attempts" "$limit"
fi
Both operands must be valid integers for the shell implementation. Validate or constrain input before passing it to a numeric comparison. If you only need the length of a string, GNU test also accepts -l STRING, although a shell parameter expansion such as ${#value} is often easier to read and is not part of this command's portable expression vocabulary.
5. Combine checks with shell control operators
For two conditions, prefer shell control operators. They short-circuit and make the grouping visible:
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 describes binary -a and -o, but warns that they are inherently ambiguous. Use separate test commands with && or || instead. The shell then decides the control flow, rather than an overloaded expression parser guessing how operands group.
Parentheses can group a test expression, but the shell sees parentheses as syntax first. They must be escaped, for example \( ... \). In ordinary scripts, separate commands are usually clearer and avoid another quoting distraction.
6. Remember what the status means
test prints nothing for a normal check. Status 0 means the expression was true; status 1 means it was false. A larger status can indicate 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 available. Every command replaces $?. In an if statement, put the test directly in the condition so the shell branches on the right command.
7. Avoid the option trap
The installed manpage says that [ honours --help and --version, while test treats those words as ordinary non-empty strings. Your shell builtin may behave differently 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 identify the command selected by the shell. Use /usr/bin/[ --version only when you specifically need the external GNU bracket program. Do not use test --version as a portable version probe.
File tests follow symbolic links except for -h and -L, which test whether the path itself is a symbolic link. That difference matters when checking a deployment path: -f asks whether the target resolves to a regular file, while -L asks whether the link exists.
Done means
- You know whether your shell selected a builtin or
/usr/bin/test. - You use quoted values and the correct file, string or integer operator.
- You combine conditions with
&&and||instead of ambiguous-aand-o. - You read status 0 as true and capture
$?before running another command. - You have not mistaken
test --versionfor a reliable version check.