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:
ifbegins the conditional statement.thenmarks the beginning of the commands to execute when the condition succeeds.fimarks 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.
0means 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:
| Operator | Meaning |
|---|---|
-eq | Equal to |
-ne | Not equal to |
-lt | Less than |
-le | Less than or equal to |
-gt | Greater than |
-ge | Greater 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:
- If the temperature is below
10, the first branch runs. - Otherwise, Bash checks whether it is below
25. - If neither condition succeeds, the
elsebranch 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
ifandthenbranch. - An
ifwith anelsebranch. - One or more
elifbranches. - 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
| Keyword | Purpose |
|---|---|
if | Begins a conditional statement |
then | Begins the commands for a successful condition |
elif | Tests another condition if earlier conditions fail |
else | Provides a fallback branch |
fi | Ends 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.