Most programmers write their first Bash if statement using [ ] (or sometimes [[ ]]), run into a space-in-variable bug, spend an hour searching Google for a fix, and eventually hear about [[ ]] as some kind of magic solution without fully understanding how the two actually differ. I was one of those people. I had written a script that parsed commands perfectly from the command line, but the moment I wrapped it inside an if conditional using a variable containing spaces, the entire script went haywire. No useful errors. It simply behaved incorrectly. That single afternoon taught me more about Bash brackets than any manual page ever could.
This article is intended for anyone who has written Bash conditionals and felt like they were making an educated guess about whether to use [ ], [[ ]], or something else entirely. Whether you are new to scripting or have been writing shell scripts for years while quietly avoiding this topic, we are going to break it down clearly so you fully understand how each bracket type works and when to use it.
Real Scenario:
Picture this: you write a script that reads a hostname from a config file. The hostname has a trailing space. Your condition is if [ $HOSTNAME = "webserver" ] and the whole script fails silently. No error, no warning, just wrong. This exact situation trips up experienced sysadmins because bash expands $HOSTNAME before evaluating the condition, and the space breaks the argument parsing entirely.
- Works perfectly when you test manually at the terminal
- Fails silently inside a script or cron job
- No error message points you to the bracket
If any of that sounds familiar, keep reading. This is the article that explains exactly why it happens and how to stop it.
Before we get into examples, it helps to have a solid base. If you are newer to bash conditionals in general, the complete bash if statement guide covers the if/then/fi structure in detail and pairs well with what we are doing here.
What Bash Single vs Double Brackets Actually Are
When you write if [ condition ], that square bracket is not bash syntax. It is a command. Specifically, it is the test command. You can actually write if test $age -gt 18 and it works the same way. The bracket is just a shorthand that has been around since the early days of Unix and was later standardized by POSIX.
The problem is that [ ] was designed to be portable across all shells. That portability comes at a cost. It does not handle some modern things well, specifically unquoted variables with spaces, pattern matching, regex, and logical operators like && and || inside the condition itself.
So bash introduced [[ ]] as a shell keyword, not a command. Because it is processed by bash before word splitting happens, it sidesteps most of the traps that catch people with [ ]. It is more powerful, more forgiving, and available in any modern bash script. The trade-off is that it is bash-specific. Scripts using [[ ]] will not run under plain sh, dash, or POSIX shells.
Note:
[[ ]] was first introduced in the Korn Shell (ksh) and later adopted by bash. It is a bash keyword, meaning bash interprets it directly rather than passing it to a child process. This is what makes it immune to word splitting.
Where [ ] Gets You Into Trouble
Here is the classic trap. A variable holds a value with a space in it. You forget to quote it inside [ ]. Bash expands the variable first, then tries to parse the result as arguments to the test command. It sees more words than it expected and throws an error or evaluates incorrectly.
This is called word splitting and it catches people constantly because the same script works fine when the variable has no spaces. The bug only appears in edge cases, which makes it harder to catch in testing.
LinuxTeck.com
# This script demonstrates the word-splitting trap
filename="my file.log"
# BROKEN — do not do this
if [ $filename = "my file.log" ]; then
echo "File matched"
else
echo "No match"
fi
No match
Common Mistake:
Using $filename unquoted inside [ ] when the value contains spaces. Bash expands the variable first, turning [ $filename = "my file.log" ] into [ my file.log = "my file.log" ], which passes four arguments to test instead of three. The result is a "too many arguments" error or silent mismatch.
Fix: Always quote variables inside [ ] like this: if [ "$filename" = "my file.log" ]. Or switch to [[ ]] where quoting is optional because word splitting does not happen inside it.
What [[ ]] Unlocks That [ ] Cannot
Once you switch to [[ ]], several things just start working that previously required workarounds. Word splitting does not apply inside [[ ]], so unquoted variables are safe. You can use && and || directly inside the brackets without needing to chain multiple [ ] tests. And you get access to pattern matching and regex, which single brackets simply do not support.
Here is the same script from above, corrected with [[ ]]:
LinuxTeck.com
# Same check, now using [[ ]] — word splitting is not an issue
filename="my file.log"
if [[ $filename == "my file.log" ]]; then
echo "File matched"
else
echo "No match"
fi
Clean, no errors, no edge case surprises. You will also notice the == operator works inside [[ ]] for string equality, which reads more naturally than the single = typically preferred in standard [ ] tests for strict POSIX compatibility. Both work inside [[ ]], but == feels more explicit and is harder to confuse with assignment.
Compound conditions become much cleaner too. Instead of writing [ "$a" -gt 5 ] && [ "$b" -lt 10 ], you can write [[ $a -gt 5 && $b -lt 10 ]] as a single unit. Less noise, same logic. You can read more about structuring these in the bash conditional statements guide.
LinuxTeck.com
# Compound condition inside [[ ]] using &&
age=25
score=88
if [[ $age -ge 18 && $score -gt 80 ]]; then
echo "Eligible: age and score both qualify"
else
echo "Does not qualify"
fi
Pattern Matching Inside [[ ]] — The Part Most Articles Skip
This is where [[ ]] becomes genuinely useful for real scripting work, and it is the part that most introductory articles barely touch. Inside [[ ]], the right-hand side of a == comparison is treated as a glob pattern. That means *, ?, and character classes like [abc] all work without quoting the right side.
This is completely different from what you can do with [ ], where == does only exact string matching. No patterns, no wildcards.
LinuxTeck.com
# Pattern matching with == inside [[ ]]
# The right side is treated as a glob pattern
logfile="error_2026_05.log"
if [[ $logfile == error_* ]]; then
echo "This is an error log file"
fi
if [[ $logfile == *.log ]]; then
echo "File has .log extension"
fi
if [[ $logfile == error_????_??.log ]]; then
echo "Filename matches date pattern YYYY_MM"
fi
File has .log extension
Filename matches date pattern YYYY_MM
One important detail: do not quote the pattern on the right side. If you write [[ $logfile == "error_*" ]], bash treats the right side as a literal string, not a pattern. The quotes suppress glob expansion. This is one of those rules that is easy to get backwards when you are used to always quoting strings.
Regex matching with =~ is even more powerful. Instead of glob patterns, you can use full POSIX extended regular expressions. This is invaluable for validating user input.
LinuxTeck.com
# Input validation using regex with =~
read -p "Enter an IP address: " ip
if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "Valid IP format: $ip"
else
echo "Invalid IP address format"
exit 1
fi
Valid IP format: 192.168.1.10
Note:
When using =~, do not quote the regex pattern on the right side. Quoting it turns it into a literal string match, which defeats the whole purpose. Also, the special array variable BASH_REMATCH stores the full match in ${BASH_REMATCH[0]} and any capture groups in ${BASH_REMATCH[1]}, ${BASH_REMATCH[2]}, etc. This is useful for extracting parts of a matched string.
Real Scripts That Use This in Production
Pattern matching inside [[ ]] earns its keep most clearly in log monitoring and automated deployment scripts. Here is a practical example: a script that scans a rotating log file, filters lines that match an error pattern, and either alerts or continues quietly. This is the kind of thing that runs in a cron job every few minutes on a production server.
One important design note before you read the script: the loop uses process substitution (< <(tail ...)) rather than a pipe. This is intentional. If you wrote tail ... | while ... do, the loop would run in a subshell and any variable changes inside it (like ERRORS_FOUND=1) would be lost the moment the loop exits. Process substitution keeps the loop in the current shell so variables persist correctly. It requires #!/bin/bash and a standard Linux environment, both of which this script already has.
LinuxTeck.com
# Log monitor — scans the last 100 lines and flags error patterns
# Designed to run from cron every 5 minutes
LOGFILE="/var/log/app/application.log"
ALERT_EMAIL="ops@example.com"
ERRORS_FOUND=0
if [[ ! -f $LOGFILE ]]; then
echo "Log file not found: $LOGFILE"
exit 1
fi
while IFS= read -r line; do
if [[ $line =~ (ERROR|CRITICAL|FATAL) ]]; then
echo "[ALERT] $line"
ERRORS_FOUND=1
fi
done < <(tail -n 100 "$LOGFILE")
if [[ $ERRORS_FOUND -eq 1 ]]; then
echo "Errors detected. Notifying $ALERT_EMAIL"
# mail -s "App Error Alert" $ALERT_EMAIL < /tmp/alert.txt
else
echo "No critical errors in last 100 lines"
fi
[ALERT] 2026-05-23 14:22:45 ERROR - Failed to write to /var/data/cache
Errors detected. Notifying ops@example.com
The key line here is [[ $line =~ (ERROR|CRITICAL|FATAL) ]]. Without [[ ]] and =~, you would need to pipe each line through grep, which spins up a subprocess per line and makes the script much slower on large log files. Keeping it inside bash with =~ is genuinely faster and cleaner. You can see how this connects to the broader automation picture in the bash exit codes and error handling guide, since production scripts like this should always exit with meaningful codes.
When to Actually Use [ ] Instead
After all of that, you might be wondering if there is ever a reason to use single brackets. There is, and being honest about this matters.
If you are writing a script that needs to run on minimal systems where bash may not be available, or if you are targeting POSIX shell specifically (scripts that start with #!/bin/sh rather than #!/bin/bash), then [ ] is your only option. Alpine Linux containers, for example, use dash as the default sh, and dash does not support [[ ]]. If your script starts with #!/bin/sh and uses [[ ]], it will fail on those systems.
For anything that runs on a known bash environment, meaning most Linux servers, CI pipelines, or desktop systems, just use [[ ]]. Quote your variables out of habit anyway. It is a cleaner default and the extra safety margins do not cost you anything.
Tip:
A quick way to check what shell is actually running your script: add echo $BASH_VERSION near the top during development. If you get a version number, you are in bash and [[ ]] is available. If you get nothing, you are in a POSIX shell and need to stick to [ ] with careful quoting. You can also check bash quoting rules to make sure your variable handling is solid either way.
One more thing worth knowing: file test operators like -f, -d, -r, and -x work identically in both [ ] and [[ ]]. There is no advantage either way for those specific tests, so you will often see them mixed in older scripts. That is fine. The critical differences only show up with string comparisons, pattern matching, and compound conditions.
Questions I Get Asked About This All the Time
Why does my script say "too many arguments" when I compare a variable?
Your variable almost certainly contains a space, and you are using [ ] without quoting it. When bash expands the variable, the space splits it into multiple words, and the test command receives more arguments than it expects. Either quote the variable: [ "$var" = "value" ], or switch to [[ $var == "value" ]] where word splitting does not apply.
Can I use regex inside single brackets?
No. The =~ operator is only available inside [[ ]]. Single brackets only support exact string comparison with = and !=, plus the standard numeric and file test operators. If you need regex, you either use [[ =~ ]] or pipe to grep.
Do I still need to quote variables inside double brackets?
Technically no, because word splitting does not happen inside [[ ]]. But quoting is still a good habit. It makes your intent clear and prevents edge cases with variables that are empty or contain glob characters. The only place you must not quote is the right-hand side of == or =~ when you want pattern or regex matching, because quotes suppress that behavior.
My script works fine in the terminal but fails when run from cron. Could brackets be the cause?
Possibly, but the more likely cause is a missing shebang or a PATH issue. Cron runs with a minimal environment, so commands your shell finds easily may not be available. Check that your script starts with #!/bin/bash, not #!/bin/sh, and that any commands you call use full paths. If you are using [[ ]] with a #!/bin/sh shebang, that is your problem.
What does BASH_REMATCH actually contain after a regex match?
After a successful [[ $var =~ pattern ]] match, ${BASH_REMATCH[0]} holds the entire matched portion of the string. Each set of parentheses in your regex creates a capture group, stored in ${BASH_REMATCH[1]}, ${BASH_REMATCH[2]}, and so on. This is useful when you want to extract a specific part of a matched string, like pulling a date out of a log line.
Is there a performance difference between [ ] and [[ ]]?
In most scripts the difference is negligible. In bash, both [ ] and [[ ]] run without forking an external process. The real difference is that [[ ]] is a shell keyword processed before word splitting, while [ ] is a builtin that still goes through bash's normal argument parsing. In a tight loop running thousands of iterations the parsing overhead of [ ] can add up slightly, but for typical sysadmin scripts it is not a meaningful factor. Choose based on features and safety, not speed.
Summary
Now that you have both bracket types straight, you can stop guessing and start writing conditionals that behave predictably. The short version: use bash single vs double square brackets based on what you actually need. Single brackets when POSIX portability is a hard requirement. Double brackets for everything else, especially when your variables might contain spaces, when you need pattern matching, or when you want regex validation with =~. Both forms work for numeric comparisons and file tests, so those are not the deciding factor.
The mistake that trips people up most is not knowing about word splitting until it bites them. Now you have seen it happen, understand why it happens, and know exactly how to fix it. That puts you ahead of a lot of people writing bash scripts in production right now. For the full picture on how bash handles errors and exit codes in scripts like the log monitor above, the official bash reference manual is the definitive source. From here, get comfortable with the conditional statement patterns in bash if elif else examples and start applying these bracket rules to scripts you actually run.
Related Articles
- Bash If Statement: Complete Guide with Examples (Part 11 / 34)
- Bash Conditional Statements Explained (Part 13 / 34)
- Bash Script Exit Codes and Error Handling (Part 5 of 34)
- Bash Quoting Rules for Cleaner Shell Scripts (Part 15 / 34)
- Linux Shell Scripting Command Cheat Sheet
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.