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 called test) is POSIX. It works in sh, 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.

  • Bash
  • Scripting
  • Linux