How to Use Conditional Expressions in Bash

How to Use Conditional Expressions in Bash

How to Use Conditional Expressions in Bash

I’ll admit it took me longer than I’d like to admit to stop confusing [, [[, and (( )) in Bash. Early on I’d copy-paste conditionals from old scripts without really understanding why one worked in one place and broke in another. Once I sat down and actually learned the differences, conditional logic in Bash stopped being a source of mysterious bugs and became one of the more predictable parts of my scripts. This article covers everything I wish someone had explained to me clearly from the start.

The Three Main Ways to Write Conditionals in Bash

Bash gives you three distinct syntaxes for conditional expressions, and each has its own quirks:

  1. [ expression ] — the POSIX-compatible test command, also usable as test expression.
  2. [[ expression ]] — Bash’s extended test command, more powerful and generally safer.
  3. (( expression )) — arithmetic evaluation, used specifically for numeric comparisons.

Understanding when to use each one is the key to writing conditionals that actually behave the way you expect.

Step 1: Basic String Comparisons

#!/usr/bin/env bash

name="Alice"

if [ "$name" = "Alice" ]; then
    echo "Hello, Alice!"
else
    echo "Who are you?"
fi

Note the single = for string equality inside [ ]. You can also use != for inequality:

if [ "$name" != "Bob" ]; then
    echo "You are not Bob."
fi

Step 2: Why Quoting Matters Inside ‘[ ]’

This is one of the most common sources of bugs I see in Bash scripts. If a variable is unset or contains spaces and isn’t quoted, [ ] can break in confusing ways:

name=""
if [ $name = "Alice" ]; then    # BROKEN if $name is empty
    echo "Match"
fi

Without quotes, an empty $name expands to nothing, leaving [ = "Alice" ], which is a syntax error because [ sees too few arguments. Always quote your variables inside [ ]:

if [ "$name" = "Alice" ]; then
    echo "Match"
fi

Step 3: Using ‘[[ ]]’ for Safer, More Powerful Conditionals

[[ ]] is a Bash keyword (not an external command like [), and it handles quoting issues and word-splitting far more gracefully:

#!/usr/bin/env bash

name="Alice Marie"

if [[ $name == "Alice Marie" ]]; then
    echo "Full match"
fi

if [[ $name == Alice* ]]; then
    echo "Starts with Alice"
fi

Notice two things: [[ ]] doesn’t require quoting $name to avoid word-splitting issues (though I still quote it as a habit for consistency and clarity), and it supports pattern matching with * directly, which [ ] does not.

Step 4: Regex Matching with ‘[[ =~ ]]’

[[ ]] also supports full regular expression matching using =~:

#!/usr/bin/env bash

email="user@example.com"

if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "Valid email format."
else
    echo "Invalid email format."
fi

This is far more powerful than anything [ ] can do, and it’s one of the main reasons I default to [[ ]] for any conditional beyond the simplest string comparison.

Step 5: Numeric Comparisons

For numbers, you have two options: the test-style operators inside [ ] or [[ ]], or full arithmetic evaluation with (( )).

Using test-style numeric operators:

age=20

if [ "$age" -ge 18 ]; then
    echo "Adult"
fi

The common numeric test operators are -eq (equal), -ne (not equal), -gt (greater than), -ge (greater or equal), -lt (less than), and -le (less or equal).

Using arithmetic evaluation instead:

age=20

if (( age >= 18 )); then
    echo "Adult"
fi

(( )) lets you use familiar mathematical operators (>=, <=, >, <, ==, !=) instead of the -ge/-le style flags, and it also supports full arithmetic like addition, subtraction, and modulo directly inside the condition.

Step 6: Combining Conditions with Logical Operators

Inside [[ ]], you can combine multiple conditions with && and || directly:

#!/usr/bin/env bash

age=25
has_id=true

if [[ "$age" -ge 18 && "$has_id" == "true" ]]; then
    echo "Entry allowed."
else
    echo "Entry denied."
fi

With the older [ ] syntax, you’d typically need separate bracket groups joined with -a/-o (which are largely discouraged now) or chain separate [ ] commands with shell-level &&/||:

if [ "$age" -ge 18 ] && [ "$has_id" = "true" ]; then
    echo "Entry allowed."
fi

I strongly prefer the [[ ]] version for readability once more than one condition is involved.

Step 7: File Test Conditions

Bash includes a rich set of file test operators, usable in both [ ] and [[ ]]:

#!/usr/bin/env bash

FILE="/etc/hosts"

if [ -e "$FILE" ]; then
    echo "File exists."
fi

if [ -f "$FILE" ]; then
    echo "It's a regular file."
fi

if [ -d "/etc" ]; then
    echo "It's a directory."
fi

if [ -r "$FILE" ]; then
    echo "It's readable."
fi

if [ -w "$FILE" ]; then
    echo "It's writable."
fi

if [ -x "/bin/ls" ]; then
    echo "It's executable."
fi

if [ -s "$FILE" ]; then
    echo "File is non-empty."
fi

These file test flags come up constantly in real scripts — checking whether a config file exists before sourcing it, verifying a directory is writable before attempting a backup, or confirming a script has execute permissions before trying to run it.

Step 8: Case Statements as an Alternative to Long If-Chains

For matching a single variable against many possible values, case is often cleaner than a long chain of if/elif:

#!/usr/bin/env bash

read -p "Enter environment (dev/staging/prod): " env

case "$env" in
    dev)
        echo "Using development configuration."
        ;;
    staging)
        echo "Using staging configuration."
        ;;
    prod)
        echo "Using production configuration. Proceed with caution."
        ;;
    *)
        echo "Unknown environment: $env"
        exit 1
        ;;
esac

case also supports pattern matching, so you can match multiple values or wildcards per branch:

case "$env" in
    dev|development)
        echo "Development mode."
        ;;
    prod*)
        echo "Some flavor of production."
        ;;
esac

Real-World Use Cases

Automation Example: A Pre-Flight Check Script

#!/usr/bin/env bash
set -euo pipefail

CONFIG_FILE="/etc/myapp/config.yml"
DATA_DIR="/var/lib/myapp"
MIN_DISK_MB=500

if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "ERROR: Config file not found at $CONFIG_FILE"
    exit 1
fi

if [[ ! -d "$DATA_DIR" ]]; then
    echo "ERROR: Data directory not found at $DATA_DIR"
    exit 1
fi

AVAILABLE_MB=$(df --output=avail -m "$DATA_DIR" | tail -1 | tr -d ' ')

if (( AVAILABLE_MB < MIN_DISK_MB )); then
    echo "ERROR: Insufficient disk space. Need ${MIN_DISK_MB}MB, have ${AVAILABLE_MB}MB."
    exit 1
fi

echo "All pre-flight checks passed. Starting application..."

This combines file existence checks, directory checks, and a numeric comparison to validate available disk space, exactly the kind of layered conditional logic that shows up in real deployment automation.

Best Practices

Troubleshooting Common Issues

“[: too many arguments” or “[: unary operator expected” errors — This almost always means an unquoted variable expanded to nothing or to multiple words. Quote your variables: [ "$var" = "value" ] instead of [ $var = "value" ].

Numeric comparison with -eq fails on a variable that “looks like” a number — Check for hidden whitespace, non-numeric characters, or an empty value in the variable; -eq and friends require genuinely integer values.

Regex matching with =~ doesn’t behave as expected — Remember not to quote the regex pattern itself (only the variable being tested), since quoting the pattern can cause Bash to treat it as a literal string rather than a regex in some Bash versions.

A case statement isn’t matching a pattern I expect it to — Double-check you’re using shell glob patterns (*, ?, [...]), not full regex syntax, since case patterns follow glob rules, not the =~ regex rules used in [[ ]].

Common Mistakes to Avoid

Frequently Asked Questions

What’s the real difference between [ and [[? [ is effectively an external command (or a shell builtin mimicking one) that follows stricter POSIX rules, requiring careful quoting and offering no pattern matching. [[ is a Bash keyword parsed specially by the shell, offering safer handling of unquoted variables, glob pattern matching, and regex support via =~.

Should I ever use (( )) for something other than plain arithmetic? (( )) is specifically built for arithmetic contexts. While it can technically evaluate conditions with logical operators like &&, its real strength is numeric comparisons and math, and using it for string-based logic will lead to confusing behavior since strings get treated as numbers (usually zero) unless they look like valid numeric expressions.

Is [[ ]] portable to all shells, like sh or dash? No. [[ ]] is a Bash (and some other advanced shells like zsh, ksh) extension, not part of POSIX sh. If you need a script to run under strict POSIX sh, you’ll need to stick with [ ] and its more limited feature set.

Can I negate a condition in Bash? Yes, using ! before the expression: if [[ ! -f "$file" ]]; then echo "File does not exist"; fi checks the opposite of the file existence test.

Summary

Conditional expressions are the backbone of decision-making in Bash scripts, and understanding the distinct roles of [ ], [[ ]], and (( )) removes a huge amount of guesswork from writing them. Use [[ ]] as your default for string comparisons and pattern/regex matching, reach for (( )) when you’re doing genuine arithmetic, and fall back to case statements when you’re matching one variable against several possible values. Getting comfortable with these distinctions turns conditional logic from a recurring source of subtle bugs into one of the more reliable parts of your scripts.

References

Exit mobile version