Conditionals (if & test)
Scripts become powerful the moment they can make decisions. Instead of blindly running the same commands every time, a script can check “does this file exist?” or “is this number bigger than that one?” and react accordingly. In Bash, the if statement plus a test (a small expression that is either true or false) is how you do that. This page shows you how if works, the difference between the old [ ] test and the modern [[ ]] test, and the most common comparisons you will actually use on an Ubuntu server.
How if decides true or false
Most languages test a true/false value. Bash is different: an if statement runs a command and looks at that command’s exit code (a number a program returns when it finishes; 0 means success, anything else means failure). If the exit code is 0, Bash treats it as true and runs the then block.
This is why you can write if grep ...; then. There is nothing magical about [ ] — it is itself a command (called test) that returns 0 or 1.
#!/usr/bin/env bash
if grep -q "nginx" /etc/passwd; then
echo "The nginx user exists"
else
echo "No nginx user found"
fi
Output:
The nginx user exists
Remember:
0means success/true and non-zero means failure/false. This is the opposite of most programming languages, where0is “false”. It trips up almost everyone at first.
The basic if / elif / else shape
The full form lets you check several conditions in order. elif means “else if”. Bash checks each branch top to bottom and runs the first one that is true.
#!/usr/bin/env bash
free_mb=$(free -m | awk '/^Mem:/ {print $7}')
if [[ "$free_mb" -lt 200 ]]; then
echo "Critical: only ${free_mb} MB free"
elif [[ "$free_mb" -lt 500 ]]; then
echo "Warning: ${free_mb} MB free"
else
echo "OK: ${free_mb} MB free"
fi
Output:
OK: 1843 MB free
The then, elif, else, and fi keywords are required. fi is just “if” spelled backwards, and it closes the block.
[ ] vs [[ ]] — which to use
Bash actually gives you two test syntaxes. [ ] is the original POSIX test command and works in every shell. [[ ]] is a Bash keyword (a built-in feature of the shell) that is safer and more capable. In Bash scripts, prefer [[ ]].
| Feature | [ ] (test) | [[ ]] (Bash) |
|---|---|---|
| Available in | All POSIX shells (sh, dash) | Bash, Zsh only |
| Unquoted empty variable | Can break / error | Safe |
| Word splitting on variables | Yes (dangerous) | No |
&& and ` | ` inside | |
Pattern matching with == | No | Yes ([[ $x == ng* ]]) |
Regex matching with =~ | No | Yes |
The classic bug: with [ ], an empty or space-containing variable can make the test fail to parse. [[ ]] does not split or glob the values inside it, so it is much harder to misuse.
If you write a script with
#!/usr/bin/env bash, use[[ ]]. Only fall back to[ ]when the script must run under plain/bin/sh(for example a Debian package maintainer script), because Ubuntu’s/bin/shisdash, which does not understand[[ ]].
String comparisons
Inside [[ ]], use == (or =) to compare strings, and != for “not equal”. Always quote your variable so a value with spaces still counts as one string.
#!/usr/bin/env bash
env="production"
if [[ "$env" == "production" ]]; then
echo "Deploying to PROD — be careful"
fi
# Pattern matching (the right side is a glob, not a literal)
host="web-prod-01"
if [[ "$host" == web-* ]]; then
echo "$host is a web server"
fi
Output:
Deploying to PROD — be careful
web-prod-01 is a web server
| Test | Meaning |
|---|---|
[[ "$a" == "$b" ]] | Strings are equal |
[[ "$a" != "$b" ]] | Strings are not equal |
[[ -z "$a" ]] | String is empty (zero length) |
[[ -n "$a" ]] | String is not empty |
Numeric comparisons
Strings and numbers use different operators. For numbers, use the two-letter flags below. Using < or > for numbers in [ ] compares them as text (so “10” looks smaller than “9”), which is a common bug.
| Operator | Meaning |
|---|---|
-eq | equal |
-ne | not equal |
-lt | less than |
-le | less than or equal |
-gt | greater than |
-ge | greater than or equal |
#!/usr/bin/env bash
count=$(ls -1 /var/log/nginx/*.log 2>/dev/null | wc -l)
if [[ "$count" -gt 0 ]]; then
echo "Found $count nginx log files"
else
echo "No nginx logs found"
fi
Output:
Found 4 nginx log files
File tests — does it exist before I touch it?
File tests are the most useful conditionals in DevOps. Before you read, edit, or delete something, check that it is actually there. This avoids errors and prevents you from acting on the wrong path.
| Test | True when |
|---|---|
-e path | Path exists (file or directory) |
-f path | Path exists and is a regular file |
-d path | Path exists and is a directory |
-r path | File is readable |
-w path | File is writable |
-x path | File is executable |
-s path | File exists and is not empty |
-L path | Path is a symbolic link |
A real example: only reload Nginx if its config file is present and the syntax is valid.
#!/usr/bin/env bash
set -euo pipefail
config="/etc/nginx/sites-available/default"
if [[ -f "$config" ]]; then
echo "Config found, checking syntax..."
if sudo nginx -t; then
sudo systemctl reload nginx
echo "Nginx reloaded"
else
echo "Syntax error — not reloading" >&2
exit 1
fi
else
echo "Config $config does not exist" >&2
exit 1
fi
Output:
Config found, checking syntax...
nginx: configuration file /etc/nginx/nginx.conf test is successful
Nginx reloaded
Combining conditions
Inside [[ ]] you can join tests with && (and) and || (or). You can also negate any test with !.
#!/usr/bin/env bash
logdir="/var/log/myapp"
if [[ -d "$logdir" && -w "$logdir" ]]; then
echo "Log directory is ready to write"
fi
if [[ ! -d "$logdir" ]]; then
mkdir -p "$logdir"
echo "Created $logdir"
fi
Output:
Created /var/log/myapp
Best Practices
- Use
[[ ]]in Bash scripts; reserve[ ]only for/bin/sh(dash) compatibility. - Always double-quote variables inside tests, e.g.
[[ "$x" == "" ]], to handle empty values and spaces safely. - Use the numeric operators (
-eq,-lt,-gt) for numbers and==/!=for strings — never mix them. - Check that a file or directory exists (
-f,-d) before reading, editing, or deleting it. - Put
set -euo pipefailat the top of scripts so an unexpected failure stops the script instead of charging ahead. - Keep conditions readable: nest
ifblocks or use&&/||rather than cramming everything into one giant test. - Send error messages to standard error with
>&2andexit 1so other tools can detect the failure.