Looking to upgrade your Bash knowledge, or maybe just struggling to grasp Bash If Statements?
Well, look no further! In this article, we'll dive deep into Bash if
statements, and discuss:
if else
elif
And a whole lot more, so that by the end of this guide, you'll be able to add more logic to your scripts, while also creating really useful stuff for your daily job.
Alright so a quick few things up top.
I'm assuming if you landed on this tutorial, you know what Bash Shell Scripting is, but just in case, let's quickly make sure we're on the same page.
So let's break it down.
if
, elif
, and else
statements are all used for decision making which is one of the most fundamental concepts in computer programming.
This is also known as 'program flow control', and it allows us to have logic in our scripts, so that they execute a section of code only when a particular criteria is met.
Simple so far, but lets dive deeper...
Reserved words are words that have a specific meaning to the Bash shell, and will either begin or end the shells compound commands.
For example
Here
if
then
elif
else
, andfi
Are all Bash reserved words, and usually used alongside some condition as a sort of comparison operation.
For example
If we look at the image again from earlier:
Basically, the above code says something like this:
elif
insteadI promise it will get easier to understand as we go through.
Before going any further though, there are a few other important things to know and remember.
The conditions are evaluated sequentially.
This means that once a test condition returns True, the remaining conditions will not be evaluated anymore and program control moves to the end of the if
construct, so after the fi
line.
fi
fi
is just if
written backwards, and ends the if
construct which is in fact a shell compound command.
Remember that each Bash compound command starts and ends with some reserved words. if
and fi
in this case.
In Bash True
means a return status of zero, while False
means a return status different from zero.
if
statementNote that we can have only one if
statement and it’s not mandatory to always have elif or else statements.
However, it is mandatory to have a whitespace before and after the square brackets that enclose the testing condition.
fi
delimitersAlso, the fi
reserved word at the end delimiters the entire if
, elif
, and else
programming structure. It’s similar to a closing curly brace "}" in other programming languages like C or Java.
Phew!
Again, this will become more clear as we work though, so let’s go to coding and see some examples, so you can see this logic working, and how it changes the code when used.
Below, we're creating a script that takes a regular file as an argument and displays its contents, like so:
1 #!/bin/bash
2 cat $1
As you can see, $1
is the first argument of the script, and I’m also giving it a regular file as an argument:
dda@victus:~/lab/bash$ chmod 700 display.sh
dda@victus:~/lab/bash$ ./display.sh urls.txt
www.google.com
www.twitter.com
www.yahoo.com
www.youtube.com
www.wikipedia.com
www.netflix.com
www.ubuntu.com
So that worked fine.
But what will happen if instead of giving the script an existing file as an argument, I give it a directory or a non-existing file?
Well, in that case the cat
command will return an error.
dda@victus:~/lab/bash$ ls -ld project
drwxrwxr-x 2 dda dda 4096 Oct 18 14:32 project
dda@victus:~/lab/bash$ ./display.sh project
cat: project: Is a directory
OK, so that means some sort of testing is necessary, and here's where we can start using some conditional logic.
I want to display the contents of the first argument of the script only if that argument is of type regular file, so I’m adding an if
statement with a test condition:
1 #!/bin/bash
2 if [ -f "$1" ]
3 then
4 cat $1
5 fi
-f
is a test condition that returns true
if what follows is a regular file.
This means that the piece of code between then
and fi
will be evaluated and run only if $1
, so the first argument of the script is of type regular file.
Sidenote: If you want to see all of the testing conditions that can be used, then run:
man test
A final remark about this code is that I’ve enclosed the $1
variable in the test condition in double quotes, but do you know why I did it this way?
Its because the result of an expansion that does not occur within double quotes is subject to word splitting.
Therefore, I used double quotes because I don't want the result of the $1
expansion to be subject to word splitting. This way it allows whitespaces in the filename, which is a small detail but it can make a difference in some cases.
Later in this article, I'll also show you another way to prevent word splitting, but before then, let’s run the script again, giving it a directory as an argument or a file that doesn’t exist.
dda@victus:~/lab/bash$ ls -ld project
drwxrwxr-x 2 dda dda 4096 Oct 18 14:32 project
dda@victus:~/lab/bash$ ./display.sh project
dda@victus:~/lab/bash$ ./display.sh non_existing
As you can see, there are no errors because the cat
command was not executed, since the test condition returned false. Huzzah!
So, let’s go ahead and add a second test condition.
In this one, if the passed argument is a directory, not a file, then I’ll run ls -l
on it.
#!/bin/bash
if [ -f "$1" ]
then
echo "The argument is a file, displaying its contents ..."
sleep 1
cat $1
elif [ -d "$1" ]
then
echo "The argument is a directory, running ls -l ..."
sleep 1
ls -l $1
else
echo "The argument ($1) is neither a file nor a directory."
fi
Also, I'm running the new script:
dda@victus:~/lab/bash$ ls -ld project
drwxrwxr-x 2 dda dda 4096 Oct 18 14:32 project
dda@victus:~/lab/bash$ ./display.sh project
The argument is a directory, running ls -l ...
total 0
-rw-rw-r-- 1 dda dda 0 Oct 18 14:33 a.cpp
dda@victus:~/lab/bash$ ./display.sh urls.txt
The argument is a file, displaying its contents ...
www.google.com
www.twitter.com
www.yahoo.com
www.youtube.com
www.wikipedia.com
www.netflix.com
www.ubuntu.com
dda@victus:~/lab/bash$ ./display.sh non_existing
The argument (non_existing) is neither a file nor a directory.
Finally, I want to tell you something about single square brackets, [ ]
vs. double square brackets [[ ]]
.
The examples shown here use single square brackets for test conditions. This is the old, classical, and portable type of testing.
There is however a new, updated version of testing that uses double square brackets as they are considered safer.
How are they safer?
Basically, double square brackets prevent word splitting of string variables that contain spaces, so you don’t need to double quote string variables, even though that’s good practice.
[[
also have some other nice features, like regular expression matching, but for the moment though don’t focus too much on this. You can use single square brackets if you don’t know what to use. I just wanted you to be aware of this option.
Let’s go ahead and see how to work with numbers and if
test conditions.
To see the available tests, we can run man test
and see all the comparison operators that can be used for integer comparison.
(Remember that in Bash there are only integers.)
The arithmetic operators used for test conditions are:
-eq
is used to compare whether two integers are equal or not. It’s the equivalent of == in other programming languages-ge
will test whether the integer on the left side is greater than or equal to the integer on the right side. (It’s the equivalent of >=
in other programming languages)-gt
will test whether the integer on the left side is greater than the integer on the right side. (It’s the equivalent of >
in other programming languages)-le
will test whether the integer on the left side is less than or equal to the integer on the right side. (It’s the equivalent of <=
in other programming languages-lt
will test whether the integer on the left side is less than the integer on the right side. It’s the equivalent of <
in other programming languages-ne
will return true if the operands are not equalLet's see an example:
#!/bin/bash
read -p "Enter your age: " age
if [[ $age -lt 18 ]]
then
echo "You are minor!"
elif [[ $age -eq 18 ]]
then
echo "Congratulations, you've just become major!"
else
echo "You are major!"
fi
The script will prompt the user for their age and will display a message according to the value entered.
dda@victus:~/lab/bash$ chmod 700 age.sh
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 10
You are a minor!
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 18
Congratulations, you've just become a major!
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 70
You are a major!
OK, so lets talk about multiple conditions and nested if
statements.
If we look at the script that we have so far, it prompts the user for a number, and tests whether the number is less than 18, equal to 18, or is greater than 18.
The script works just fine, but there's a problem with the logic of the script.
If the user enters a very large number for the age, like 200, the script will print out: "you are a major!", and we can all agree that there’s no one that old still alive. (Unless vampires etc).
So, we can solve the problem by adding a nested if
statement or even better, using the logical AND
operator. All I have to do is add another condition so that the age is always positive and less than 100.
The logical OR
and AND
operators allow you to use multiple conditions in the same if
statement.
With this in mind, I'm changing the script:
#!/bin/bash
read -p "Enter your age: " age
if [[ $age -lt 18 && $age -ge 0 ]]
then
echo "You are a minor!"
elif [[ $age -eq 18 ]]
then
echo "Congratulations, you've just become a major!"
elif [[ $age -gt 18 && $age -le 100 ]]
then
echo "You are a major!"
else
echo "Invalid age!"
fi
Alright, so let's run the script:
dda@victus:~/lab/bash$ ./age.sh
Enter your age: -7
Invalid age!
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 5
You are a minor!
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 18
Congratulations, you've just become a major!
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 71
You are a major!
dda@victus:~/lab/bash$ ./age.sh
Enter your age: 200
Invalid age!
Note that if you use single square brackets you can use -a
instead of &&
and -o
, instead of ||
Let’s take a look at a new example.
Let’s return to the script created at the beginning of this article, which takes an argument and checks if the argument is of type file or directory.
If it’s a file it displays its contents using cat
, and if it’s a directory it lists its contents using ls -l
.
We’ve already tested it and it worked just fine. However, the problem is that the argument is mandatory. This means that if the script is run without an argument it will display the wrong message and say that the argument is neither a file nor a directory.
dda@victus:~/lab/bash$ ./display.sh
The argument () is neither a file nor a directory.
It would be better to check if there’s an argument, and only in that case to start testing if it’s a file or a directory and execute the commands.
Now, keep in mind that the number of arguments of a script is stored in a special predefined variable named $#
, so I’m adding a testing condition to see if there’s exactly one argument.
I'm also changing single brackets [ ]
to double brackets [[ ]]
.
#!/bin/bash
if [[ $# -eq 1 ]]
then
if [ -f "$1" ]
then
echo "The argument is a file, displaying its contents ..."
sleep 1
cat $1
elif [ -d "$1" ]
then
echo "The argument is a directory, running ls -l ..."
sleep 1
ls -l $1
else
echo "The argument ($1) is neither a file nor a directory."
fi
else
echo "The script should be run with an argument"
fi
And I’m testing it by running the script with a different number of arguments.
dda@victus:~/lab/bash$ ./display.sh
The script should be run with an argument
dda@victus:~/lab/bash$ ./display.sh project
The argument is a directory, running ls -l ...
total 0
-rw-rw-r-- 1 dda dda 0 Oct 18 14:33 a.cpp
dda@victus:~/lab/bash$ ./display.sh users.txt
The argument is a file, displaying its contents ...
u1:gr1
u2:gr2
u3:gr3
dda@victus:~/lab/bash$ ./display.sh non_existing
The argument (non_existing) is neither a file nor a directory.
It’s much better!
By the way, this is called a nested if
, because there’s an inner if
statement inside an outer one.
Let's move on!
When automating different common administrative tasks using shell scripts, you will often need to compare the output of a command, which is a string, to another string value to see whether they are equal or not and based on the result, to act accordingly.
Let’s see how to compare strings in Bash.
Two strings are equal when they have the same length and contain the same sequence of characters in the same order.
The comparison operator for strings is the single equal sign if you use the if
statement with single square brackets, or the double equal sign if you use the if
statement with double square brackets.
There’s also the inequality operator that returns true if the strings are not equal. As in any other programming language, this operator consists of an exclamation mark and an equal sign (!=)
.
Let’s move on and see how to check if a string contains a substring.
There are multiple ways to do that, but one approach is to surround the substring with asterisk symbols, which means matching all characters.
I’m creating a new script for this example:
#!/bin/bash
str1="Nowadays, Linux powers the servers of the Internet."
if [[ “$str1” == *"Linux"* ]]
then
echo "The substring Linux is there."
else
echo "The substring Linux IS NOT there"
fi
And if we check that...
dda@victus:~/lab/bash$ ./substring.sh
The substring Linux is there.
Sometimes you want to check if a string resulting from another operation like a command substitution is empty or not.
You can do this by using an if statement with -n
or -z
.
-n
returns True if the string length is non-zero-z
returns True if the string length is zero 1 #!/bin/bash
2 my_str="abc"
3 if [[ -z "$my_str" ]]
4 then
5 echo "String is zero length."
6 else
7 echo "String IS NOT zero length."
8 fi
9
10 my_str=""
11 if [[ -n "$my_str" ]]
12 then
13 echo "String IS NOT zero-length"
14 else
15 echo "String is zero length"
16 fi
Let's run it!
dda@victus:~/lab/bash$ ./empty_string.sh
String IS NOT zero length.
String is zero length
Phew, that was a lot to cover, but hopefully by now you understand bash if statements a little better, and can get to work adding them into your own projects.
If you still have questions about Bash and want to learn more, check out my complete Bash Scripting course.
I guarantee that it's is the most comprehensive and up-to-date online resource to learn Bash Scripting. Plus, we'll give you the exact steps you need to take to get hired as a SysAdmin, DevOps Engineer, or Network Engineer no matter what level of experience you have.
Alternatively, you can also check out the first lessons of my Bash course for free here.