Bash — The if Conditional Statement and the fi keyword

Learn how Bash’s `if` statement makes decisions based on command results, comparisons, and conditions. Explore `then`, `elif`, `else`, and `fi`, with practical examples and tips for avoiding common mistakes.

Bash — The if Conditional Statement and the fi keyword

Imagine writing a script that needs to make a decision. Perhaps it should check whether a file exists before reading it, determine whether a user entered the correct password, or decide what to do when a command succeeds or fails.

Without conditional logic, a script would simply execute commands one after another, regardless of what happened along the way. That might be fine for a script that prints Hello, world!, but it is not particularly useful when things get interesting.

This is where Bash's if statement comes in.

The if statement lets your script choose which commands to execute based on the result of a condition. It works together with keywords such as then, else, elif, and fi to form one of the most fundamental structures in shell scripting.

Let's explore how it works, starting with the basics.

1. What Is the if Statement?

In programming, a conditional statement allows a program to make decisions. Bash provides the if statement for this purpose.

Its basic structure looks like this:

if condition; then
    # Commands to execute if the condition succeeds
else
    # Commands to execute if the condition fails
fi

There are three essential parts to understand:

  • if begins the conditional statement.
  • then marks the beginning of the commands to execute when the condition succeeds.
  • fi marks the end of the entire conditional statement.

The else section is optional. If present, its commands execute when the condition fails.

Notice the slightly unusual keyword at the end: fi. It is simply if spelled backward. Bash uses several keywords to mark the beginning and end of control structures, and fi closes an if statement in the same way that done closes a loop.

One important detail: if does not evaluate whether an expression is mathematically true or false in the way you might expect from other programming languages. Instead, Bash executes a command and checks its exit status.

That distinction is the key to understanding Bash conditionals.

2. How Bash Decides Whether a Condition Succeeds

In Bash, every command finishes with an exit status, which is an integer.

  • 0 means success.
  • A non-zero value means failure or another unsuccessful outcome.

This convention may seem backward at first. Many programming languages use true and false, but Unix-like systems traditionally treat zero as success.

For example:

if true; then
    echo "The command succeeded."
else
    echo "The command failed."
fi

The true command returns an exit status of 0, so Bash executes the first echo command.

Now consider this example:

if false; then
    echo "The command succeeded."
else
    echo "The command failed."
fi

The false command returns a non-zero status, so Bash executes the else branch instead.

The commands true and false make this behavior easy to demonstrate. In real scripts, however, your conditions will usually involve tests, file operations, or other commands.

3. Checking Numeric Values

Let's start with the first example from our old draft: checking the value of a variable.

Suppose we have a variable called a and want to determine whether it equals 5.

#!/usr/bin/env bash

a=5

if [ "$a" -eq 5 ]; then
    echo "Variable a equals 5."
else
    echo "Variable a does not equal 5."
fi

Because a contains the number 5, the condition succeeds and Bash prints:

Variable a equals 5.

Let's break down the condition:

[ "$a" -eq 5 ]

The square brackets are the test command in a convenient alternative form. They evaluate a condition and return an exit status indicating whether the test succeeded.

The -eq operator compares integer values. Some other numeric comparison operators are:

OperatorMeaning
-eqEqual to
-neNot equal to
-ltLess than
-leLess than or equal to
-gtGreater than
-geGreater than or equal to

For example, to check whether a variable contains a positive number:

if [ "$a" -gt 0 ]; then
    echo "The number is positive."
fi

There is no else branch here. That is perfectly valid: if the condition fails, Bash simply skips the commands inside the ifblock and continues after fi.

A small but important detail: use numeric operators such as -eq when comparing integers. The = operator, by contrast, is commonly used for string comparisons.

4. Comparing Strings

Numbers are only part of the story. Scripts frequently need to compare text, such as environment names, usernames, command-line arguments, or configuration values.

Our original example used a string:

#!/usr/bin/env bash

a="text"

if [ "$a" = "text" ]; then
    echo 'Variable a contains the string "text".'
else
    echo 'Variable a does not contain the string "text".'
fi

Here, = checks whether the two strings are equal.

Other useful string tests include:

  • = or == — the strings match.
  • != — the strings differ.
  • -z "$value" — the string is empty.
  • -n "$value" — the string is not empty.

For example, a script might require a configuration value before continuing:

#!/usr/bin/env bash

environment="${ENVIRONMENT:-}"

if [ -n "$environment" ]; then
    echo "Using environment: $environment"
else
    echo "No environment specified."
fi

The expression ${ENVIRONMENT:-} expands to the value of ENVIRONMENT, or to an empty string if the variable is unset. The -n test then checks whether the resulting string is non-empty.

Notice that the variable is quoted inside the test. Quoting expansions is a good habit because it prevents word splitting and pathname expansion from changing how many arguments the test receives.

What about [[ ... ]]?

Bash also provides the [[ ... ]] conditional expression:

if [[ "$a" == "text" ]]; then
    echo "The strings match."
fi

This is Bash-specific syntax and often makes conditions easier to write safely. It also supports features such as pattern matching and regular expressions.

For example:

filename="report.log"

if [[ "$filename" == *.log ]]; then
    echo "This is a log file."
fi

Here, *.log is a shell pattern. It matches strings that end in .log.

Both [ ... ] and [[ ... ]] are useful. If you are writing Bash scripts, [[ ... ]] is often a good default for string tests and more complex conditions. If you need POSIX shell compatibility, use the portable [ ... ] form and avoid Bash-specific syntax.

5. Using a Command as the Condition

One of the most useful features of Bash's if statement is that the condition does not have to be a comparison. You can use almost any command whose exit status tells you whether an operation succeeded.

For example, suppose you want to copy a file and report whether the operation succeeded.

Our old draft contained this example:

if $(cp file newfile); then
    echo "Copy succeeded."
else
    echo "Copy failed."
fi

However, this version contains an important mistake: $(...) performs command substitution. It runs the command and substitutes its output into the surrounding command line. It does not directly test the exit status of cp.

The correct version is much simpler:

#!/usr/bin/env bash

if cp file newfile; then
    echo "Copy succeeded."
else
    echo "Copy failed."
fi

Bash executes cp file newfile and checks its exit status. If the copy succeeds, cp returns 0, and the first message is printed. If the copy fails—for example, because the source file does not exist—the exit status is non-zero, and the second message is printed.

You can use the same pattern with many commands:

if grep -q "ERROR" application.log; then
    echo "Errors were found in the log."
else
    echo "No matching errors were found."
fi

The -q option tells grep to operate quietly without printing matching lines. Its exit status indicates whether a match was found, or whether an error occurred.

Keep in mind that a non-zero status does not always mean a technical failure. For example, grep returns a non-zero status when no match is found, which is a normal result in many scripts.

This direct approach is usually clearer than running a command first and then inspecting $?.

6. The elif Keyword: More Than Two Choices

So far, our examples have offered two possible outcomes: one for success and one for failure.

What if we need three or more choices?

Bash provides elif, short for “else if.” It allows you to test another condition when the preceding condition fails.

#!/usr/bin/env bash

temperature=25

if [ "$temperature" -lt 10 ]; then
    echo "It's cold."
elif [ "$temperature" -lt 25 ]; then
    echo "The weather is mild."
else
    echo "It's warm."
fi

The conditions are evaluated in order:

  1. If the temperature is below 10, the first branch runs.
  2. Otherwise, Bash checks whether it is below 25.
  3. If neither condition succeeds, the else branch runs.

Since the temperature is 25, the script prints It's warm.

You can use as many elif branches as necessary:

if [ "$status" -eq 0 ]; then
    echo "Success."
elif [ "$status" -eq 1 ]; then
    echo "A general error occurred."
elif [ "$status" -eq 2 ]; then
    echo "Invalid arguments."
else
    echo "An unexpected status was returned."
fi

Each branch is checked only if all previous conditions have failed. Once a condition succeeds, Bash executes that branch and skips the remaining branches.

If you find yourself writing a long chain of conditions to select an action based on one value, consider whether a casestatement might make the code easier to read.

7. Combining Conditions

Sometimes a decision depends on more than one condition.

With Bash's [[ ... ]], you can combine tests using logical operators:

  • && means AND: both conditions must succeed.
  • || means OR: at least one condition must succeed.
  • ! means NOT: reverse the result of a condition.

For example, a deployment script might continue only if the target environment is production and a deployment package exists:

if [[ "$environment" == "production" && -f "$package" ]]; then
    echo "Ready to deploy."
else
    echo "Deployment requirements are not met."
fi

The -f test checks whether the specified path refers to a regular file.

You can also use OR:

if [[ "$environment" == "staging" || "$environment" == "production" ]]; then
    echo "This is a deployment environment."
fi

And NOT:

if [[ ! -f "$package" ]]; then
    echo "The package file is missing."
fi

Logical operators are useful, but avoid packing too many unrelated conditions into a single line. If a condition becomes difficult to understand, split it into smaller tests or add a brief comment.

8. Nesting if Statements

A conditional statement can contain another conditional statement. This is called nesting.

For example, imagine a script that first checks whether a file exists and then checks whether it is readable:

#!/usr/bin/env bash

file="config.ini"

if [[ -f "$file" ]]; then
    if [[ -r "$file" ]]; then
        echo "The file exists and is readable."
    else
        echo "The file exists but is not readable."
    fi
else
    echo "The file does not exist."
fi

Each if must have its own matching fi. The inner fi closes the inner conditional, while the final fi closes the outer one.

Nesting is perfectly valid, but deep nesting can make a script difficult to follow. When possible, you can combine conditions or use an early exit to keep the main logic simple.

For example:

#!/usr/bin/env bash

file="config.ini"

if [[ ! -f "$file" ]]; then
    echo "Error: file not found." >&2
    exit 1
fi

if [[ ! -r "$file" ]]; then
    echo "Error: file is not readable." >&2
    exit 1
fi

echo "Processing configuration..."

This version handles errors immediately and leaves the main processing logic at the top level. The >&2 redirection sends error messages to standard error, while exit 1 terminates the script with a non-zero exit status.

Choose whichever structure makes the intended logic easiest to understand.

9. The fi Keyword: Closing the Conditional

Now that we have explored if, let's give its closing keyword the attention it deserves.

The fi keyword marks the end of an if statement. It is not a separate command that performs an operation on its own; it is part of Bash's syntax.

Consider this small example:

if true; then
    echo "Hello!"
fi

echo "The conditional has ended."

Bash executes the first message inside the conditional and then continues with the command after fi.

The same closing keyword is used whether the conditional contains:

  • Only an if and then branch.
  • An if with an else branch.
  • One or more elif branches.
  • Nested conditional statements.

For example, all of the following structures are valid:

# A simple conditional
if true; then
    echo "Success."
fi
# A conditional with an alternative
if false; then
    echo "Success."
else
    echo "Failure."
fi
# A conditional with multiple alternatives
if false; then
    echo "First branch."
elif true; then
    echo "Second branch."
else
    echo "Fallback branch."
fi

The number of fi keywords must match the number of corresponding if statements. When a script contains nested conditionals, indenting each level consistently makes it much easier to see which fi closes which if.

10. Common Mistakes

Even a small conditional can cause problems when its syntax or logic is slightly wrong. Here are some mistakes worth watching for.

Forgetting then

Incorrect:

if [ "$value" = "ready" ]
    echo "Ready."
fi

Correct:

if [ "$value" = "ready" ]; then
    echo "Ready."
fi

The then keyword separates the condition from the commands that follow it. You can place it on a new line instead, but it must be present.

Forgetting fi

Incorrect:

if true; then
    echo "Hello!"

Correct:

if true; then
    echo "Hello!"
fi

Without the closing keyword, Bash reaches the end of the script while still expecting the conditional structure to be completed.

Using the wrong comparison operator

Incorrect:

if [ "$number" > 10 ]; then
    echo "Greater than 10."
fi

Inside [ ... ], the > character can be interpreted as a shell output-redirection operator rather than a numeric comparison.

For numeric comparisons, use -gt:

if [ "$number" -gt 10 ]; then
    echo "Greater than 10."
fi

Alternatively, with Bash, you can use arithmetic evaluation:

if (( number > 10 )); then
    echo "Greater than 10."
fi

Testing command output instead of its exit status

Incorrect:

if $(some_command); then
    echo "Success."
fi

Correct:

if some_command; then
    echo "Success."
fi

Command substitution, written as $(...), captures a command's standard output. It is not the normal way to check whether that command succeeded.

Confusing = and -eq

Use = for string equality:

if [[ "$name" == "alice" ]]; then
    echo "Hello, Alice."
fi

Use -eq for integer equality with [ ... ]:

if [ "$count" -eq 5 ]; then
    echo "There are five items."
fi

Using the right operator makes the script's intention clear.

11. A Practical Example

Let's put these ideas together in a small script that checks whether a file can be processed.

#!/usr/bin/env bash

file="${1:-}"

if [[ -z "$file" ]]; then
    echo "Usage: $0 FILE" >&2
    exit 2
elif [[ ! -e "$file" ]]; then
    echo "Error: '$file' does not exist." >&2
    exit 1
elif [[ ! -f "$file" ]]; then
    echo "Error: '$file' is not a regular file." >&2
    exit 1
elif [[ ! -r "$file" ]]; then
    echo "Error: '$file' is not readable." >&2
    exit 1
else
    echo "Processing '$file'..."
    # Add the actual file-processing commands here.
fi

echo "Finished checking the file."

Save it as check-file.sh and make it executable:

chmod +x check-file.sh

Then run it with a filename:

./check-file.sh config.ini

The script checks the supplied argument in order. If no filename is supplied, it displays usage instructions and exits. If the path is missing, is not a regular file, or is not readable, it reports the problem and exits with an error status. Otherwise, it reaches the processing branch.

Because exit terminates the script, the final echo is reached only when the script gets past the checks without exiting.

This is a useful pattern for real-world scripts: validate inputs early, report problems clearly, and keep the successful path easy to follow.

12. A Quick Reference

KeywordPurpose
ifBegins a conditional statement
thenBegins the commands for a successful condition
elifTests another condition if earlier conditions fail
elseProvides a fallback branch
fiEnds the conditional statement

A minimal if statement looks like this:

if command; then
    # Run when command returns status 0
fi

A complete conditional with alternatives looks like this:

if condition1; then
    commands1
elif condition2; then
    commands2
else
    fallback_commands
fi

For more information, Bash's built-in help can explain the shell syntax and related commands:

help if
help test
help [

You can also consult the Bash Reference Manual for a detailed description of conditional constructs and conditional expressions.

Conclusion

The if statement is one of the building blocks of Bash scripting. It allows a script to respond to command results, compare values, validate files, and choose between alternative actions.

The most important thing to remember is that Bash evaluates a condition by checking a command's exit status: zero means success, and non-zero means the condition did not succeed. The command might be a test such as [ ... ], a Bash conditional expression such as [[ ... ]], or an ordinary command such as cp or grep.

The fi keyword completes the structure. It may not do anything exciting on its own, but without it, Bash cannot properly close the conditional statement.

Once you understand how if, then, elif, else, and fi work together, you have one of the essential tools for writing scripts that can do more than blindly follow instructions. They can make decisions—and occasionally make the right ones.

Read next

Bash — The declare Built-in

Learn how Bash’s declare built-in lets you control variable attributes, create arrays, define read-only or integer variables, export values, and inspect how Bash treats your variables.

Bash — The fg Built-in

Learn how Bash’s fg built-in brings background and suspended jobs back to the foreground. Discover how to use fg, bg, and jobs, understand job numbers, and manage running commands without restarting them.

Bash — The bg Built-in

Learn how Bash’s bg built-in lets you resume stopped jobs in the background without interrupting your terminal session. Discover how to use Ctrl+Z, bg, and jobs, understand job specifications, and learn when to use bg instead of fg or &.