Conditionals let scripts make decisions. Does the file exist? Did the backup succeed? Is the disk over 90%? Bash’s if has a few quirks, such as spaces inside brackets and different operators for numbers and strings. Once you know them, it’s straightforward.
Syntax
if CONDITION; then
…
elif OTHER_CONDITION; then
…
else
…
fi
The condition is a command. if runs it and checks its exit status: 0 means true and anything else means false. [ … ] and [[ … ]] are just commands that test things.
Testing a command directly
if grep -q 'error' /var/log/app.log; then
echo "errors found"
fi
if ! systemctl is-active --quiet nginx; then
systemctl restart nginx
fi
if mysqldump shop > shop.sql; then
echo "backup ok"
else
echo "backup FAILED" >&2
exit 1
fi
Don’t write if [ $(grep …) ] or check $? afterwards. Put the command in the if itself.
[ ] vs [[ ]]
[ … ](also calledtest) is POSIX. It works insh, dash and every shell.[[ … ]]is a Bash keyword. It’s safer and more capable: no word splitting on variables,&&/||inside, pattern matching and regex.
In Bash scripts, prefer [[ ]]. In #!/bin/sh scripts, use [ ] and quote every variable.
Spaces are required: [[ -f "$file" ]], not [[-f "$file"]].
File tests
| Test | True if |
|---|---|
-e path |
exists |
-f path |
is a regular file |
-d path |
is a directory |
-L path |
is a symlink |
-s path |
exists and isn’t empty |
-r / -w / -x path |
readable / writable / executable |
a -nt b |
a is newer than b |
if [[ ! -d /var/backups/db ]]; then
mkdir -p /var/backups/db
fi
[[ -f .env ]] || { echo ".env missing" >&2; exit 1; }
String tests
| Test | True if |
|---|---|
"$a" == "$b" |
equal (= also works) |
"$a" != "$b" |
not equal |
-z "$a" |
empty |
-n "$a" |
not empty |
$a == pattern* |
glob match (in [[ ]] only, pattern unquoted) |
$a =~ regex |
regex match (in [[ ]] only) |
if [[ -z "$DB_PASSWORD" ]]; then
echo "DB_PASSWORD is not set" >&2; exit 1
fi
if [[ "$ENV" == prod* ]]; then echo "production"; fi
if [[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then echo "looks like IPv4"; fi
Number tests
Inside [ ] and [[ ]], numbers use -eq -ne -lt -le -gt -ge. < and > compare strings there, so 10 < 9 would be true.
if [[ "$count" -gt 100 ]]; then …; fi
Arithmetic context (( )) is clearer for numbers:
usage=$(df --output=pcent / | tail -1 | tr -dc '0-9')
if (( usage >= 90 )); then
echo "Disk at ${usage}%!"
fi
Combining conditions
if [[ -f "$f" && -s "$f" ]]; then …; fi
if [[ "$1" == "-h" || "$1" == "--help" ]]; then usage; fi
if ! [[ -w "$dir" ]]; then …; fi
With [ ], combine separate tests: [ -f "$f" ] && [ -s "$f" ].
Short forms
[[ -d "$dir" ]] || mkdir -p "$dir"
command -v docker >/dev/null || { echo "docker not installed"; exit 1; }
a && b || c is not a true if/else. If b fails, c runs too. Use a real if when that matters.
case: matching one value against many patterns
case "$1" in
start) systemctl start myapp ;;
stop) systemctl stop myapp ;;
restart|reload) systemctl restart myapp ;;
*.tar.gz) tar -xzf "$1" ;;
*) echo "usage: $0 {start|stop|restart}"; exit 1 ;;
esac
case is cleaner than a long elif chain, and it’s the standard way to handle script arguments.
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
[ $var == x ] with empty var |
“unary operator expected” | Quote: [ "$var" = x ] or use [[ ]] |
[[$x -eq 1]] |
Missing spaces | [[ $x -eq 1 ]] |
[[ $a > $b ]] for numbers |
String comparison | (( a > b )) |
if [ grep -q x f ] |
Brackets aren’t needed | if grep -q x f |
= vs == in sh |
== isn’t POSIX |
Use = in [ ] |
Related: Bash for loops, Bash variables and Bash scripting tutorial.