Bash Conditionals Compared: When to Use test, [, and [[
Conditional logic is a cornerstone of robust shell scripting, allowing scripts to make decisions and alter their flow based on various conditions. In Bash, the primary tools for evaluating these conditions are the test command, single brackets [ ], and double brackets [[ ]]. While they often appear interchangeable to the casual observer, subtle yet critical differences exist in their behavior, capabilities, security implications, and shell compatibility.
Understanding these distinctions is vital for writing efficient, secure, and portable Bash scripts. This article will thoroughly explore each of these conditional constructs, providing practical examples and detailing their unique characteristics to help you choose the right tool for every scripting scenario. We'll cover their historical context, advanced features, and common pitfalls, equipping you with the knowledge to wield Bash conditionals with confidence.
The test Command: The Foundation
The test command is one of the oldest and most fundamental ways to evaluate conditions in shell scripts. It's a built-in command in most modern shells and is part of the POSIX standard, making it highly portable. test evaluates an expression and returns an exit status of 0 (true) or 1 (false).
Basic Usage
The test command takes one or more arguments, which form the expression to be evaluated. It checks for file attributes, string comparisons, and integer comparisons.
# Check if a file exists
if test -f "myfile.txt"; then
echo "myfile.txt exists and is a regular file."
fi
# Check if two strings are equal
NAME="Alice"
if test "$NAME" = "Alice"; then
echo "Name is Alice."
fi
# Check if one number is greater than another
COUNT=10
if test "$COUNT" -gt 5; then
echo "Count is greater than 5."
fi
Common test Operators
- File Operators:
-f(regular file),-d(directory),-e(exists),-s(not empty),-r(readable),-w(writable),-x(executable). - String Operators:
=(equal),!=(not equal),-z(string is empty),-n(string is not empty). - Integer Operators:
-eq(equal),-ne(not equal),-gt(greater than),-ge(greater than or equal),-lt(less than),-le(less than or equal).
Tip: Always quote variables used with test (e.g., "$NAME") to prevent issues with word splitting and pathname expansion if the variable's value contains spaces or glob characters.
Single Brackets [ ]: The test Alias
The single bracket [ ] construct is, in essence, an alternative syntax for the test command. In many shells, [ is simply a hard link or a built-in alias to test. The key difference is that [ requires a closing ] as its last argument to function correctly. Like test, it is POSIX compliant.
Syntax and Semantics
# Equivalent to test -f "myfile.txt"
if [ -f "myfile.txt" ]; then
echo "myfile.txt exists and is a regular file using [ ]."
fi
# Equivalent to test "$NAME" = "Alice"
NAME="Bob"
if [ "$NAME" != "Alice" ]; then
echo "Name is not Alice."
fi
Notice the mandatory space after [ and before ]. These are treated as separate arguments to the [ command.
Quoting Variables: A Critical Detail
Because [ ] is fundamentally the test command, it inherits the same behaviors regarding word splitting and pathname expansion. This means unquoted variables can lead to unexpected behavior or security vulnerabilities.
Consider this example:
#!/bin/bash
INPUT="file with spaces.txt"
# DANGEROUS: Unquoted variable will cause issues if INPUT contains spaces
# The shell will perform word splitting, treating "file" and "with spaces.txt" as separate arguments
# leading to a syntax error or incorrect evaluation.
# if [ -f $INPUT ]; then echo "Found"; else echo "Not found"; fi
# CORRECT: Quote the variable to treat it as a single argument
if [ -f "$INPUT" ]; then
echo "'file with spaces.txt' exists."
else
echo "'file with spaces.txt' does not exist or is not a regular file."
fi
Without quotes, $INPUT would expand to file with spaces.txt, and [ -f file with spaces.txt ] would be interpreted as a syntax error by the [ command because -f expects only one operand. Quoting ensures that $INPUT is passed as a single argument, "file with spaces.txt".
Word Splitting and Pathname Expansion Dangers
Both test and [ are subject to the shell's default behaviors of word splitting and pathname expansion (globbing). If a variable contains spaces or glob characters (*, ?, [ ]) and is unquoted, the shell will expand it before test or [ see the arguments. This can lead to incorrect comparisons or even executing unintended commands (if glob characters match existing files).
Double Brackets [[ ]]: The Modern Bash Keyword
The double bracket [[ ]] construct is a Bash keyword (also supported by Ksh and Zsh), not an external command or an alias. This distinction is crucial, as it allows [[ ]] to behave differently and offer enhanced functionality and improved safety compared to test or [ ].
Enhanced Functionality
[[ ]] introduces several powerful features not available with test or [:
-
No Word Splitting or Pathname Expansion: Variables within
[[ ]]generally do not need to be quoted (though it's often good practice to do so for clarity). The shell handles the contents of[[ ]]as a single unit, preventing word splitting and pathname expansion. This significantly reduces common scripting errors and security risks.```bash
No need to quote variables (though still safe to do so)
INPUT="file with spaces.txt"
if [[ -f $INPUT ]]; then # $INPUT is treated as a single string here
echo "'$INPUT' exists."
fi
``` -
Globbing for String Comparison: The
==and!=operators perform pattern matching (globbing) rather than strict string equality when used inside[[ ]]. This means you can use*,?, and[]as wildcards.```bash
FILE_NAME="my_document.txt"
if [[ "$FILE_NAME" == *".txt" ]]; then # Checks if FILE_NAME ends with .txt
echo "It's a text file!"
fiNote: For strict string equality without globbing, use
testor[ ]with=or ensure no glob characters are present in the right-hand side of
==in [[ ]](or quote the right-hand side if it contains literal glob characters you want to match literally).
```
-
Regular Expression Matching: The
=~operator allows you to perform regular expression matching.```bash
bash
IP_ADDRESS="192.168.1.100"
if [[ "$IP_ADDRESS" =~ ^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$ ]]; then
echo "Valid IP format."
fiImportant: The regex pattern on the right-hand side of =~ should generally NOT be quoted
if it contains characters that would otherwise be treated as glob patterns.
If the regex is in a variable, it should be unquoted as well.
Pattern example: ^[A-Za-z]+$
```
-
Logical Operators
&&and||:[[ ]]supports the more intuitive C-style logical operators&&(AND) and||(OR) for combining multiple conditions, along with!for negation. These operators have proper short-circuit evaluation and precedence, unliketest's-aand-o.```bash
AGE=25
if [[ "$NAME" == "Alice" && "$AGE" -ge 18 ]]; then
echo "Alice is an adult."
fiif [[ "$USER" == "root" || -w /etc/fstab ]]; then
echo "Either root or can write to fstab."
fi
```
Bash-Specific Nature
While [[ ]] offers significant advantages, its main drawback is that it's a Bash/Ksh/Zsh extension and not part of the POSIX standard. This means scripts relying on [[ ]] may not be portable to sh, dash, or older/minimalist Unix-like systems.
Side-by-Side Comparison: test vs. [ vs. [[
Here's a table summarizing the key differences:
| Feature | test |
[ ] |
[[ ]] |
|---|---|---|---|
| Type | Built-in command (or external) | Built-in command (alias to test) |
Shell keyword (Bash, Ksh, Zsh) |
| POSIX Compliant | Yes | Yes | No |
Closing ] Required |
No | Yes (as last argument) | Yes (as part of keyword) |
| Word Splitting | Yes (on unquoted variables) | Yes (on unquoted variables) | No (variables treated as single string) |
| Pathname Expansion | Yes (on unquoted variables) | Yes (on unquoted variables) | No |
| Globbing (Pattern) | No (for string equality) | No (for string equality) | Yes (==, !=) |
| Regular Expressions | No | No | Yes (=~) |
| Logical AND/OR | -a, -o (precedence issues) |
-a, -o (precedence issues) |
&&, || (C-style, short-circuiting) |
| Compound Commands | Requires separate test calls |
Requires separate [ calls |
Can combine expressions directly (&&/||) |
| Variable Quoting | Mandatory for safety | Mandatory for safety | Generally not required, but good practice |
When to Use Which
Choosing the right conditional construct depends primarily on your portability requirements and the complexity of your conditional logic.
POSIX Compliance vs. Modern Bash Features
-
Use
testor[ ]when...- Portability is paramount: If your script needs to run on any POSIX-compliant shell (
sh,dash, older systems, etc.),testor[ ]are your only reliable options. - Your conditions are simple (file checks, basic string/integer comparisons).
- You are comfortable with careful quoting of all variables and avoiding
&&/||in favor of nestedifstatements ortest -a/-o(with caution).
- Portability is paramount: If your script needs to run on any POSIX-compliant shell (
-
Use
[[ ]]when...- You are writing exclusively for Bash (or Ksh/Zsh) and do not need POSIX portability.
- You require advanced features like globbing pattern matching, regular expression matching, or C-style
&&/||logical operators. - You want the enhanced safety features that prevent word splitting and pathname expansion, leading to more robust and less error-prone code.
- Your conditions involve complex logic that would be cumbersome with
test -a/-o.
Best Practices and Recommendations
-
Prioritize
[[ ]]for Bash Scripts: If your script is intended for Bash,[[ ]]is generally the preferred choice due to its increased safety, extended functionality, and more intuitive syntax for complex conditions. It drastically reduces common scripting errors related to quoting and special characters. -
Always Quote in
testand[ ]: If you must usetestor[ ]for POSIX compliance, make it a habit to always quote your variables to prevent unexpected behavior from word splitting and pathname expansion.```bash
Good practice for [ ] and test
VAR="a string with spaces"
if [ -n "$VAR" ]; then echo "Not empty"; fi
``` -
Be Mindful of
=vs.==: Intestand[ ],=is used for string equality. In[[ ]],==performs pattern matching (globbing), while=performs strict string equality if the right-hand side has no glob patterns. For consistent strict string comparison in[[ ]], it's generally safe to use==as long as you're not intentionally using glob patterns. If you need globbing,==is how you do it in[[ ]]. -
Regular Expressions with
=~: When using=~in[[ ]], the right-hand side should typically be unquoted to allow the shell to interpret it as a regular expression pattern, not a literal string to match.```bash
Unquoted regex pattern is correct for =~ in [[ ]]
if [[ "$LINE" =~ ^Error: ]]; then echo "Error found"; fi
```
Conclusion
The test command, single brackets [ ], and double brackets [[ ]] are all vital for implementing conditional logic in Bash. While test and [ ] offer POSIX portability, they demand meticulous attention to quoting and can be more prone to issues with complex expressions or variable content. In contrast, [[ ]] provides a powerful, safer, and more feature-rich environment for conditional evaluations, making it the de facto standard for modern Bash scripting, albeit at the cost of strict POSIX compliance.
By understanding their unique characteristics and applying the recommended best practices, you can write more reliable, efficient, and maintainable Bash scripts, ensuring your conditional logic behaves exactly as intended every time. For Bash-specific scripts, [[ ]] will generally lead to cleaner, safer code, while test or [ ] remain indispensable for maximum portability across diverse Unix-like environments.