Bash Conditionals Compared: When to Use test, [ , and [[
Compare test, single brackets, and double brackets so your Bash conditionals stay portable, safe, and readable.
Bash Conditionals Compared: When to Use test, [ , and [[
When a Bash conditional behaves strangely, the problem is often the construct you chose. test, [ ], and [[ ]] look similar, but they handle quoting, patterns, regex, and portability differently.
This guide compares the three forms so you can write conditionals that are safe, readable, and appropriate for the shell your script actually runs under.
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 Form
The single bracket [ ] construct is an alternative syntax for the test command. In many shells, [ is a shell built-in, and systems often also provide an external /usr/bin/[. The key difference is that [ requires a closing ] as its last argument. 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 syntax errors or incorrect comparisons when 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.# 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." fiGlobbing 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.FILE_NAME="my_document.txt" if [[ "$FILE_NAME" == *".txt" ]]; then # Checks if FILE_NAME ends with .txt echo "It's a text file!" fi # Note: For strict string equality without globbing, use `test` or `[ ]` 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.
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."
fi
# Important: 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]+$
```
4. **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, unlike `test`'s `-a` and `-o`.
```bash
AGE=25
if [[ "$NAME" == "Alice" && "$AGE" -ge 18 ]]; then
echo "Alice is an adult."
fi
if [[ "$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 are treated as single strings |
| **Pathname Expansion** | Yes, on unquoted variables | Yes, on unquoted variables | No |
| **Globbing Pattern Match** | No for string equality | No for string equality | Yes with unquoted right side of `==` or `!=` |
| **Regular Expressions** | No | No | Yes with `=~` |
| **Logical AND/OR** | `-a`, `-o` exist but are easy to misread | `-a`, `-o` exist but are easy to misread | `&&`, `||` with normal short-circuit behavior |
| **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 `test` or `[ ]` when...**
- **Portability is paramount**: If your script needs to run on any POSIX-compliant shell (`sh`, `dash`, older systems, etc.), `test` or `[ ]` 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 using shell-level `&&`/`||` outside the brackets when you need compound logic.
- **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
1. **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.
2. **Always Quote in `test` and `[ ]`**: If you *must* use `test` or `[ ]` 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
```
3. **Be Mindful of Pattern Matching**: In `test` and `[ ]`, `=` is used for string equality. In `[[ ]]`, `=` and `==` both can perform pattern matching when the right-hand side is unquoted. Quote the right-hand side when you want a literal string comparison.
4. **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
```
## Takeaway
Use `[ ]` or `test` when your script must run under POSIX `sh`. Use `[[ ]]` when your shebang is Bash and you want safer variable handling, glob matching, regex matching, and cleaner compound conditions. The main habit is simple: match the conditional syntax to the shell, and quote deliberately.