Skip to content
DevOps devops shell 6 min read

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: 0 means success/true and non-zero means failure/false. This is the opposite of most programming languages, where 0 is “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 inAll POSIX shells (sh, dash)Bash, Zsh only
Unquoted empty variableCan break / errorSafe
Word splitting on variablesYes (dangerous)No
&& and `` inside
Pattern matching with ==NoYes ([[ $x == ng* ]])
Regex matching with =~NoYes

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/sh is dash, 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
TestMeaning
[[ "$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.

OperatorMeaning
-eqequal
-nenot equal
-ltless than
-leless than or equal
-gtgreater than
-gegreater 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.

TestTrue when
-e pathPath exists (file or directory)
-f pathPath exists and is a regular file
-d pathPath exists and is a directory
-r pathFile is readable
-w pathFile is writable
-x pathFile is executable
-s pathFile exists and is not empty
-L pathPath 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 pipefail at the top of scripts so an unexpected failure stops the script instead of charging ahead.
  • Keep conditions readable: nest if blocks or use &&/|| rather than cramming everything into one giant test.
  • Send error messages to standard error with >&2 and exit 1 so other tools can detect the failure.
Last updated June 15, 2026
Was this helpful?