Bash If Then Else: Essential Syntax Guide with Examples & Common Mistakes

Look, if you're poking around Linux or macOS terminals, or even wrangling servers somewhere, writing scripts is basically unavoidable. And the heart of making those scripts actually do something useful, make decisions like a human would? That's where the bash shell if then else construct comes crashing in. It's not glamorous, maybe a bit clunky at first glance, but man, is it essential. Trying to script without it is like trying to build a house with just a hammer – possible for a tiny shed, maybe, but good luck with anything complex. Let's break this down without the fluff.

No Fancy Talk: What is If Then Else in Bash Really?

At its absolute core, the bash shell if then else structure lets your script ask a simple question: "Is this thing true?" Based on the answer (yes or no, true or false), it takes one path or another. Think of it like those old Choose Your Own Adventure books.

  • If the door is unlocked... then walk inside.
  • Else (if it's locked)... try the window.

In Bash terms, you're checking conditions: if this file exists, if that number is bigger than 10, if the user typed 'yes'. Simple logic, powerful results. I remember messing this up early on, spending ages debugging a script because I forgot the spaces around the brackets! Frustrating, but a good lesson.

Getting Your Hands Dirty: The Basic Syntax

Alright, enough theory. Let's see the raw bones of it. Here's the simplest form:

if [ condition ]
then
    # Commands to run if the condition is TRUE
fi

See that [ condition ]? That's the test. Those square brackets [ ] are actually a command (seriously, try which [ sometime) that checks whatever you put inside. Crucial detail: you must have spaces inside the brackets around your condition. Bash is weirdly picky about whitespace. Forget them, and you get cryptic errors. Ask me how I know...

Adding the "Else" Branch

What about when the condition is false? That's where else hops in:

if [ condition ]
then
    # Commands for TRUE
else
    # Commands for FALSE
fi

Now you're covering both possibilities. Imagine checking if a backup directory exists: if it does, great, start the backup; else (it doesn't exist), create it first then backup. Makes sense, right?

Handling More Choices: Enter "Elif"

Life isn't always binary. Sometimes you have multiple options. For that, bash gives you elif (short for "else if"):

if [ condition1 ]
then
    # Action for condition1 TRUE
elif [ condition2 ]
then
    # Action for condition2 TRUE (and condition1 was FALSE)
else
    # Action if NEITHER condition1 NOR condition2 were TRUE
fi

You can chain as many elif checks as you need, though it can get messy. I once saw a script with 15 elifs... not pretty and hard to debug. Sometimes a case statement is cleaner for lots of options, but if elif else handles the common 2-4 choices well.

Beyond Basic True/False: Decoding "Conditions"

This is where the real meat is. What can you actually test inside those [ ] brackets? A whole lot. Here are the bread-and-butter checks you'll use constantly:

Checking Files and Directories (Super Common!)

Need to know if a file exists before you try reading it? Bash has your back.

Condition What It Checks Example When You'd Use It
-e file File/directory exists (any type). if [ -e "/backups/last_run.log" ] Before reading a config/log file, verifying input paths.
-f file Is a regular file (not directory, symlink, etc.). if [ -f "$HOME/.bashrc" ] Before modifying a known config file.
-d file Is a directory. if [ -d "/tmp/processing" ] Before creating files inside a dir, cleaning up temp folders.
-s file File exists and has size > 0 (not empty). if [ -s "output.csv" ] Before processing data files to ensure they contain data.
-r file File exists and is readable. if [ -r "/etc/shadow" ] (usually fails!) Before trying to read sensitive files you *might* have access to.
-w file File exists and is writable. if [ -w "report.txt" ] Before attempting to write or append to a file.
-x file File exists and is executable. if [ -x "/usr/bin/curl" ] Before running a command to verify it's available and runnable.

These file tests saved my bacon more times than I can count, especially -f and -d. Accidentally treating a directory like a file? Script crashes. Check first.

Comparing Strings (Text Data)

Working with filenames, user input, configuration values? You'll be comparing strings.

Condition What It Checks Example Critical Notes
"$str1" = "$str2" Strings are equal. if [ "$USER" = "root" ] Quote variables! Prevents errors if they are empty or have spaces. Spaces around = or != are essential!
"$str1" != "$str2" Strings are not equal. if [ "$response" != "yes" ] Good for validating user input against expected values.
-z "$str" String is empty (zero length). if [ -z "$filename" ] Checking if a required variable wasn't set. Always quote!
-n "$str" String is not empty (has length). if [ -n "$error_message" ] Often used instead of ! -z. Quote it!

That quoting variables tip? Non-negotiable. If $filename is empty, [ -f $filename ] becomes [ -f ], which is a syntax error. [ -f "$filename" ] becomes [ -f "" ], which safely evaluates to false. Big difference. Got burned by that early on.

Comparing Numbers (Integers)

Loop counters, exit codes, user input numbers – time for numeric comparisons. Notice we use different operators than strings!

Condition Meaning Example Math Equivalent
$num1 -eq $num2 Equal if [ $count -eq 10 ] count == 10
$num1 -ne $num2 Not Equal if [ $exit_code -ne 0 ] exit_code != 0
$num1 -gt $num2 Greater Than if [ $disk_used -gt 90 ] disk_used > 90
$num1 -ge $num2 Greater Than or Equal if [ $age -ge 18 ] age >= 18
$num1 -lt $num2 Less Than if [ $temperature -lt 0 ] temperature < 0
$num1 -le $num2 Less Than or Equal if [ $retries -le 3 ] retries <= 3

Mixing up -eq (numbers) and = (strings) is a classic beginner mistake. Your script might seem to work sometimes (if the numbers are single digits) and fail weirdly other times. Use the right tool!

Leveling Up: Combining Conditions

Real-world checks often need multiple conditions. Bash provides logical AND (-a) and OR (-o) within [ ], but honestly? They get messy fast, especially with quoting. The modern, cleaner way is using double brackets [[ ]] and the operators && (AND) and || (OR).

Using Double Brackets [[ ]] - Less Quoting Headache

if [[ -f "$config_file" && -r "$config_file" ]]; then
    echo "Config file exists and is readable. Loading..."
    source "$config_file"
elif [[ ! -e "$config_file" ]]; then
    echo "Config file missing! Using defaults."
else
    echo "Config file exists but isn't readable! Check permissions." >&2
    exit 1
fi

See the differences?

  • No strict need for quotes around variables inside [[ ]] (though it doesn't hurt and can be safer in complex cases).
  • Use familiar && and ||.
  • Pattern matching works with == and != (e.g., [[ "$filename" == *.log ]]).
  • Regular Expression matching with =~ (e.g., [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] - basic email check).

I strongly recommend using [[ ]] for anything beyond the simplest checks. It handles more complex logic and quoting much more gracefully.

Exit Codes: The Secret Sauce for Robust Scripts

Every command you run in Linux/Unix finishes with an exit code (or return status). It's a number between 0 and 255.

  • 0 = Success
  • Anything else (1-255) = Failure (or some specific non-success state defined by the command)

The if statement inherently checks the exit code of the command it runs! This is incredibly powerful. Instead of [ ] or [[ ]], you can run any command.

if grep -q "ERROR" /var/log/syslog; then
    echo "Found errors in syslog! Sending alert..."
    send_alert "Syslog Errors"
fi

Here, the condition is the grep -q "ERROR" /var/log/syslog command itself. grep -q exits with:

  • 0 if "ERROR" is found (success).
  • 1 if "ERROR" is not found ("failure" to find it).

So the if runs the echo and send_alert only if "ERROR" exists in the log. Clean and direct!

Why Exit Codes Matter with If Then Else

Understanding exit codes is fundamental to using bash shell if then else effectively beyond simple tests. Your script's logic often hinges on whether a critical step (cp, mv, curl, make) actually worked.

cp important_data.txt /backup/
if [ $? -eq 0 ]; then
    echo "Backup succeeded! Removing original."
    rm important_data.txt
else
    echo "Backup FAILED! Exit code: $?. Original file kept." >&2
    exit 1
fi

That cryptic $?? That's a special variable holding the exit code of the *very last* command executed before it. So cp runs, then [ $? -eq 0 ] checks if it succeeded.

Here's a quick cheat sheet of common exit codes you might check:

Exit Code General Meaning Common Commands Using It
0 Success Most commands on success
1 General error Catch-all for many commands (e.g., grep not found)
2 Misuse of shell builtin bash itself (e.g., syntax error)
126 Command found but not executable Permission denied when trying to run a program
127 Command not found Typo in command name, or program not installed
130 Script terminated by Ctrl+C SIGINT signal received
137 Process killed forcibly (SIGKILL) kill -9
255* Exit status out of range Sometimes used for specific errors

(*Note: 255 is the maximum due to being stored in 8 bits; returning a number greater than 255 usually wraps around). Checking specific exit codes lets you handle different failures differently.

Putting It All Together: Real-World Bash If Then Else Examples

Let's look at some practical chunks of scripts you might actually use. These combine file tests, string checks, exit codes, and logic.

Example 1: Safe File Processing

# Check if input file exists and is readable
if [[ ! -f "$input_file" ]]; then
    echo "Error: Input file '$input_file' does not exist or is not a regular file." >&2
    exit 1
fi

if [[ ! -r "$input_file" ]]; then
    echo "Error: Cannot read input file '$input_file'. Check permissions." >&2
    exit 1
fi

# Check if output directory exists and is writable; create if needed
output_dir="./processed"
if [[ ! -d "$output_dir" ]]; then
    echo "Output directory '$output_dir' not found. Creating it..."
    mkdir -p "$output_dir" || { echo "Failed to create output dir! Aborting." >&2; exit 1; }
fi

if [[ ! -w "$output_dir" ]]; then
    echo "Error: Cannot write to output directory '$output_dir'. Check permissions." >&2
    exit 1
fi

# Process the file
echo "Processing '$input_file'..."
process_data "$input_file" > "$output_dir/result.txt"

# Check if processing succeeded
if [ $? -ne 0 ]; then
    echo "Error: Processing step failed! Check logs." >&2
    exit 1
else
    echo "Processing completed successfully. Output in '$output_dir/result.txt'"
fi

Example 2: User Input Validation

# Prompt user
read -p "Do you want to continue? (yes/no): " user_response

# Convert response to lowercase for case-insensitive check
user_response_lower=${user_response,,}

# Validate using bash shell if then else
if [[ "$user_response_lower" == "yes" || "$user_response_lower" == "y" ]]; then
    echo "Proceeding with operation..."
    # ... perform actions ...
elif [[ "$user_response_lower" == "no" || "$user_response_lower" == "n" ]]; then
    echo "Operation cancelled by user."
    exit 0
else
    echo "Invalid input: '$user_response'. Please enter 'yes' or 'no'." >&2
    exit 1
fi

Frequently Stumbled-Upon Questions (The FAQ You Actually Need)

Do I really need spaces around [, ], and operators like = or -eq?

Yes. Absolutely. Non-negotiable. if [ $var="value"] will fail. if [ $var = "value" ] (with spaces) is correct. This trips up everyone at least once. The [ is a command, and commands need spaces separating their arguments. Use [[ ]] to reduce this pain slightly.

Why do my string comparisons fail when the variable is empty?

Because you didn't quote the variable! if [ $var = "hello" ]. If $var is empty, this becomes if [ = "hello" ] - a syntax error. Always double-quote variables inside [ ] or [[ ]]: if [ "$var" = "hello" ]. If $var is empty, it becomes if [ "" = "hello" ], which correctly evaluates to false. Quoting is crucial.

What's the difference between [ ] and [[ ]]? Which should I use?

  • [ ] (or test): The classic, POSIX-compatible test command. Works in most shells. Requires strict quoting and uses -a/-o for AND/OR.
  • [[ ]]: A Bash keyword (not a command). More powerful and forgiving. Better handling of variables (less quoting anxiety), supports &&/||, pattern matching (==, !=), regex matching (=~). Generally safer and preferred for Bash scripts.

Recommendation: Use [[ ]] exclusively in your Bash scripts unless you have a specific need for POSIX compatibility (like writing a /bin/sh script). It prevents so many common quoting and logic headaches.

How do I check if a command succeeded in an if statement?

Put the command directly in the if condition! The if checks the exit code of the command immediately before the then.

if curl -s -o /dev/null https://example.com; then
    echo "Website is reachable."
else
    echo "Failed to reach website or curl error." >&2
fi

This runs curl silently (-s), dumping output to the void (-o /dev/null), and the if checks if curl exited with 0 (success). Much cleaner than running curl first and then checking $?.

Can I put multiple commands inside an if-then-else block?

Absolutely! That's the whole point. Indent the lines under then, else, or elif (or use semicolons).

if [[ "$debug_mode" == "true" ]]; then
    echo "DEBUG: Starting processing at $(date)"
    echo "DEBUG: Using config file: $config_path"
    set -x  # Turn on command tracing
fi

Do as much as you need within each branch.

Why do I sometimes see a semicolon before 'then'? Like if ...; then

It's a style thing, mainly. The semicolon ; acts as a command separator. If you write your if condition and the then keyword on the same line, you need a semicolon to tell Bash where the condition ends and the then keyword begins.

if [ -f file.txt ]; then  # Semicolon required on same line
    echo "File exists"
fi

# Or, written differently (no semicolon needed):
if [ -f file.txt ]
then
    echo "File exists"
fi

Both are valid. Putting the then on a new line avoids the need for the semicolon and some folks find it cleaner, especially for longer conditions. I generally prefer putting then on a new line unless the condition is very short.

What are common mistakes when using bash if then else?

  • Missing Spaces: Around [, ], operators (=, -eq, -lt).
  • Unquoted Variables: Leading to syntax errors or unexpected behavior when variables are empty or contain spaces/special characters. Quote them!
  • Using = for Numbers / -eq for Strings: Mixing up string and numeric comparisons.
  • Forgetting the fi: Every if must be closed with fi. Mismatched if/fi is a syntax error.
  • Overcomplicating Logic: Deeply nested if/elif/else can be hard to read. Consider breaking code into functions or using a case statement for many specific string matches.
  • Not Handling Failure Paths: Especially when checking exit codes. Always think, "What if this command fails?" and handle it gracefully with else or checking $?.

Debugging tip: Add set -x near the top of your script (or temporarily). It prints each command before it's executed, showing you exactly how variables expand and what commands are run. Helped me spot countless quoting and logic errors. Turn it off with set +x.

Going Beyond the Basics (A Tiny Peek)

While bash shell if then else handles most conditional logic, Bash has other tools:

  • Case Statements: Excellent for matching a variable against multiple specific patterns or strings. Often cleaner than a long chain of elif [[ ... ]] checks.
  • Conditional Execution with && and ||: You can chain simple actions: command1 && command2 (run command2 ONLY if command1 succeeded). command1 || command2 (run command2 ONLY if command1 failed). Useful for one-liners but can become unreadable for complex logic – stick to if then else for clarity there.

Mastering if then else is the foundation. Build that solidly first.

Look, the bash shell if then else structure might seem like a small cog, but it's the core decision-making engine of your scripts. Get comfortable with file tests (-f, -d), string comparisons (=, != - quoted!), numeric comparisons (-eq, -lt), and checking exit codes directly in the if. Embrace [[ ]] for its quoting sanity and extra features. Remember those spaces, quote your vars, and close your ifs with fi. It's not rocket science, but getting these fundamentals wrong will make scripting feel like pulling teeth. Get them right, and suddenly automating tasks feels powerful. Go write some scripts that actually make smart choices!

Leave a Comments

Recommended Article