The
test command is a legacy utility that evaluates conditional expressions, while the
[[ ... ]] and
[ ... ] constructs are syntactic sugar for it within
if statements. Converting code from an explicit
test format to the more idiomatic
if [ ... ] then ... fi or
if [[ ... ]] then ... fi structure improves readability and leverages modern Bash features like pattern matching and logical operators without external process invocation.
Understanding the Test Command
In older shell scripts, developers often used the standalone
test command to evaluate conditions. For example, checking if a file exists might look like this:
test -f /etc/passwd
if [ $? -eq 0 ]; then
echo "File exists"
fi
This approach is verbose and requires checking the exit status (
$?) explicitly. The
test command returns a zero exit status if the expression is true and non-zero otherwise. While functional, this style is considered outdated in modern Bash scripting because it spawns a separate process and lacks the clarity of structured conditional blocks.
The Standard Bracket Syntax
The most common conversion involves replacing
test with single brackets
[ ... ]. This syntax is POSIX-compliant and works in all Bourne-compatible shells, including
sh,
dash, and
bash.
To convert the previous example:
if [ -f /etc/passwd ]; then
echo "File exists"
fi
Here,
[ is actually a command (often linked to
/usr/bin/test), but it must be followed by a closing bracket
] as its last argument. Spaces are critical;
[ -f file ] is valid, but
[-f file] will fail because the shell interprets it as a single command name.
When converting multiple conditions, you can use logical operators:
if [ -f /etc/passwd ] && [ -r /etc/passwd ]; then
echo "File exists and is readable"
fi
Note that using
&& between two separate
[ commands creates two distinct processes. This is less efficient than using extended test constructs.
Extended Test Constructs in Bash
Bash provides double brackets
[[ ... ]], which offer enhanced functionality over single brackets. These are not available in pure POSIX
sh but are standard in
bash,
zsh, and
ksh.
Converting to
[[ ... ]] allows for safer string handling and advanced pattern matching:
if [[ -f /etc/passwd && -r /etc/passwd ]]; then
echo "File exists and is readable"
fi
Key advantages include:
- Logical Operators: You can use
&& and || directly inside the brackets without spawning extra processes.
- Pattern Matching: Use
=~ for regular expression matching, e.g., [[ $string =~ ^[0-9]+$ ]].
- No Word Splitting: Variables do not need quoting to prevent word splitting issues, though quoting is still good practice for safety.
For instance, comparing strings with wildcards is possible:
filename="report.txt"
if [[ $filename == *.txt ]]; then
echo "It's a text file"
fi
This feature is particularly useful when processing files, similar to tasks described in our guide on
copying image files recursively from camera folders, where renaming based on patterns is essential.
Handling Complex Conditions
When dealing with nested conditions or complex logic,
[[ ... ]] reduces ambiguity. Consider checking if a variable is set and non-empty:
var="value"
if [[ -n "$var" ]]; then
echo "Variable is set"
fi
Using single brackets, you would write
[ -n "$var" ], which works fine, but
[[ ... ]] is more robust against edge cases involving empty strings or special characters.
Best Practices for Conversion
When refactoring legacy scripts, follow these steps to ensure compatibility and correctness:
- Identify All
test Calls: Search your script for instances of test or [ ... ].
- Replace with
if [[ ... ]]: If your script is Bash-specific, prefer double brackets for better performance and features.
- Preserve POSIX Compatibility: If the script must run on systems with only
sh (like Alpine Linux containers), stick to single brackets [ ... ].
- Test Thoroughly: Run your script with
bash -x to trace execution and verify that conditions evaluate as expected.
Avoid mixing styles. Stick to one type of test construct throughout a script to maintain consistency. Also, remember that
test can still be used in places where you need just the exit status without an
if block, such as in command chaining:
test -f config.yml || echo "Config missing" > error.log
However, even here,
[ -f config.yml ] || ... is preferred for brevity.
Common Pitfalls
One frequent error during conversion is forgetting spaces around brackets. Always ensure there is a space after
[ and before
]. Another pitfall is relying on
test behavior in non-Bash shells where
[[ ... ]] is not supported. If you share scripts across different environments, validate the target shell version. For example, if you encounter issues executing commands inside a script on RHEL 7, check the shell version and compatibility settings as discussed in our article on
shell script execution issues on RHEL 7.
Frequently Asked Questions
Can I use [[ ]] in sh scripts?
No, the double bracket syntax
[[ ... ]] is a Bash extension and is not available in standard POSIX
sh. If you need your script to be portable across different Unix-like systems, use single brackets
[ ... ] instead.
Why should I avoid the standalone test command?
Using
test explicitly is unnecessary because
[ ... ] and
[[ ... ]] are built-in equivalents that integrate better with control structures like
if. It also reduces the chance of errors related to missing arguments or incorrect spacing.
How do I handle string comparisons safely?
Always quote variables inside test constructs to prevent word splitting and globbing issues. For example, use
[ "$var" = "value" ] rather than
[ $var = value ] to ensure correct evaluation even if
$var is empty or contains spaces.
What is the difference between -eq and == in tests?
Use
-eq for numeric comparisons in both single and double brackets. Use
== for string equality in double brackets, though it also works in single brackets in Bash. However,
-eq cannot be used for strings, and
== may behave differently depending on the shell version.
Is there a performance benefit to using [[ ]]?
Yes,
[[ ... ]] avoids forking a new process for each condition when using logical operators like
&& and
||. In single brackets,
[ A ] && [ B ] runs two separate processes, whereas
[[ A && B ]] handles it internally, making it faster in loops.