Programming Bash #3: Logical Operators
Last Updated on January 21, 2024 by David Both
In this series of articles we are looking at Bash as a programming language. In the second article, we explored some simple command line programming with Bash. We explored using variables and control operators. In this article we explore the five types of Bash file, string, numeric, and miscellaneous logical operators that provide execution flow control logic; different types of shell expansions.
Logical operators are the basis for making decisions in a program and executing different sets of instructions based on those decisions. This is sometimes called flow control.
Preparation
Before proceeding we need to create some test directories and files that will be used in this article. You should do this on a VM, a non-production computer, or a non-production account on your workstation. Make your home directory the PWD and execute the following instructions as a non-root user.
[dboth@testvm1 ~]$ for I in 0 1 2 3 4 5 6 7 8 9 ; do echo "This is testfile$I" > testfile$I.txt ; done
[dboth@testvm1 ~]$ mkdir -p ./testdir1/testdir2/testdir3/testdir4/testdir5 testdir6 testdir7
[dboth@testvm1 ~]$ tree
[dboth@testvm1 ~]$ cd testdir1
[dboth@testvm1 ~]$ for X in dmesg.txt dmesg1.txt dmesg2.txt dmesg3.txt dmesg4.txt ; do dmesg > $X ; done
[dboth@testvm1 testdir1]$ touch newfile.txt
[dboth@testvm1 testdir1]$ df -h > diskusage.txt
[dboth@testvm1 testdir1]$ for I in 0 1 2 3 4 5 6 7 8 9 ; do dmesg > file$I.txt ; done
The -p option for the mkdir command creates the entire specified directory hierarchy if any of them don’t exist. This prevents having to create each directory in the hierarchy individually. I use this fairly frequently and it is a great time-saver.
This now gives us a few files and directories to work with.
Logical operators
Bash has a large set of logical operators that can be used in conditional expressions. The most basic form of the if control structure is to test for a condition and then execute a list of program statements if the condition is true. There are three types of operators, file, numeric and non-numeric operators. Each operator returns true (0) as its return code if the condition is met and false (1) if the condition is not met.
The functional syntax of these comparison operators is one or two arguments with an operator that are placed within square braces. Then a list of program statements that are executed if the condition is true.
if [ arg1 operator arg2 ] ; then list
Or like this with an optional list of program statements if the condition is false.
if [ arg1 operator arg2 ] ; then list ; else list ; fi
The spaces in the comparison are required as shown. The single square braces, [ and ], are the traditional Bash symbols that are equivalent to the test command.
if test arg1 operator arg2 ; then list
There is also a more recent syntax that offers a few advantages and which some SysAdmins prefer. This format is a bit less compatible with different versions of Bash and other shells such as ksh (Korn shell).
if [[ arg1 operator arg2 ]] ; then list
File operators
One powerful set of logical operators that are part of Bash are the file operators. Figure 1 lists more than 20 different operators that can be performed on files by Bash. I use thise quite frequently in my scripts to determine the status of various attributes belonging to a file.
Operator | Description |
-a filename | True if the file exists. It can be empty or have some content but so long as it exists this will be true. |
-b filename | True if file exists and is a block special file such as a hard drive like /dev/sda or /dev/sda1. |
-c filename | True if the file exists and is a character special file such as a TTY device like /dev/TTY1. |
-d filename | This is true if the file exists and is a directory. |
-e filename | True if file exists. This is the same as -a, above. |
-f filename | True if the file exists and is a regular file as opposed to a directory a device special file or a link, among others. |
-g filename | True if the file exists and is set-group-id, SETGID. |
-h filename | This is true if file exists and is a symbolic link. |
-k filename | True if file exists and its “sticky” bit is set. |
-p filename | True if file exists and is a named pipe (FIFO). |
-r filename | True if file exists and is readable, i.e., has its read bit set. |
-s filename | True if file exists and has a size greater than zero. A file that exists but that has a size of zero will return false. |
-t fd | True if the file descriptor fd is open and refers to a terminal. |
-u filename | True if file exists and its set-user-id bit is set. |
-w filename | True if file exists and is writable. |
-x filename | True if file exists and is executable. |
-G filename | True if file exists and is owned by the effective group id. |
-L filename | True if file exists and is a symbolic link. |
-N filename | True if file exists and has been modified since it was last read. |
-O filename | True if file exists and is owned by the effective user id. |
-S filename | True if file exists and is a socket. |
file1 -ef file2 | True if file1 and file2 refer to the same device and iNode numbers. |
file1 -nt file2 | True if file1 is newer (according to modification date) than file2, or if file1 exists and file2 does not. |
file1 -ot file2 | True if file1 is older than file2, or if file2 exists and file1 does not. |
Let’s look at some examples. We will start by testing for the existence of a file.
[dboth@testvm1 testdir1]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 does not exist.
[dboth@testvm1 testdir1]$
Create a file for testing named TestFile1. For now it does not need to contain any data.
[david@testvm1 testdir1]$ touch TestFile1
Note how easy it is to change the value of the $File variable rather than a text string for the file name in multiple locations in this short CLI program.
[dboth@testvm1 testdir1]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 exists.
[dboth@testvm1 testdir1]$
Now let’s determine whether a file exists and has a non-zero length which means it contains data. We have three conditions we want to test for so we need a more complex set of tests. The three conditions are; 1, the file does not exist; 2, the file exists and is empty; and 3, the file exists and contains data. To accomplish this we need to use the elif stanza in the if-elif-else construct to test for all of the conditions.
Because these logic constructs can become complex I find it helpful to build them up one test at a time. In the following examples we will work up to testing for all three conditions. Let’s start by testing to see if the file exists and it contains data.
[david@testvm1 testdir1]$ File="TestFile1" ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
[david@testvm1 testdir1]$
That works but it is only truly accurate for one specific condition out of the three possible ones we have identified. Let’s add an else stanza so we can be somewhat more accurate, and delete the file so that we can fully test this new code.
[david@testvm1 testdir1]$ File="TestFile1" ; rm $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.
Finally, let’s add some content to the file. and test again.
[david@testvm1 testdir1]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 exists and contains data.
Delete the file so we start this next program with a file that doesn’t exist.
[david@testvm1 testdir1]$ File="TestFile1" ; rm $File
Now we add the elif stanza to discriminate between a file that does not exist and one that is empty.
[david@testvm1 testdir1]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and is empty.
Finally add some data to the file.
[david@testvm1 testdir1]$ File="TestFile1" ; echo "This is $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and contains data.
[david@testvm1 ~]$
Now we have a Bash CLI program that can test for these three different conditions and the possibilities are endless. Experiment with this some more to re-verify the code with all three conditions for the file.
It is easier to see the logic structure of the more complex compound commands if we arrange the program statements more like we would in a script that we saved in a file. Figure 2 shows how this would look. The indents of the program statements in each stanza of the if-elif-else structure help to clarify the logic.
File="TestFile1"
echo "This is $File" > $File
if [ -s $File ]
then
echo "$File exists and contains data."
elif [ -e $File ]
then
echo "$File exists and is empty."
else
echo "$File does not exist."
fi
Figure 2: The command line program rewritten as it would appear in a script.
Logic of some complexity becomes too lengthy for most CLI programs. Although any Linux or Bash built-in commands may be used in CLI programs, as the CLI programs get longer and more complex it makes sense to create a script that is stored in a file and which can be executed at any time now or in the future. We’ll get to that in the next article of this series. But for now, we have more logic operators to explore.
String comparison operators
String comparison operators enable comparison of alphanumeric strings of characters. There are only a few of these operators which are listed in Figure 3.
Operator | Description |
-z string | True if the length of string is zero. |
-n string | True if the length of string is non-zero. |
string1 == string2 or string1 = string2 | True if the strings are equal. A single = should be used with the test command for POSIX conformance. When used with the [[ command, this performs pattern matching as described above (Compound Commands) |
string1 != string2 | True if the strings are not equal. |
string1 < string2 | True if string1 sorts before string2 lexicographically1. |
string1 > string2 | True if string1 sorts after string2 lexicographically. |
First, let’s look at string length. Notice that the quotes around $MyVar in the comparison itself must be there for the comparison to work. We are still working in the home directory.
[david@testvm1 testdir1]$ MyVar="" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
[david@testvm1 testdir1]$ MyVar="Random text" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
We could also do it this way.
[david@testvm1 testdir1]$ MyVar="Random text" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar contains data.
[david@testvm1 testdir1]$ MyVar="" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar is zero length
Since we are already talking about strings and whether they are zero length or more than zero, it might make sense that we sometimes need to know the exact length. Although this is not a comparison it is related to them. Unfortunately there is no simple way to determine the length of a string. There are a couple ways to do this but I think using the expr (evaluate expression) is the easiest. Read the man page for expr for more of what it can do. Quotes are required around the string or variable being tested.
[david@testvm1 testdir1]$ MyVar="" ; expr length "$MyVar"
0
[david@testvm1 testdir1]$ MyVar="How long is this?" ; expr length "$MyVar"
17
[david@testvm1 testdir1]$ expr length "We can also find the length of a literal string as well as a variable."
70
Back to our comparison operators. I use a lot of testing in my scripts to determine whether two strings are equal, that is, identical. I use the non-POSIX version of this comparison operator
[david@testvm1 testdir1]$ Var1="Hello World" ; Var2="Hello World" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 matches Var2
[david@testvm1 testdir1]$ Var1="Hello World" ; Var2="Hello world" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 and Var2 do not match.
You can experiment some more on your own to try out these operators.
Numeric comparison operators
These numeric operators make comparisons between two numeric arguments. Like the other operator classes, most of these are easy to understand.
Operator | Description |
arg1 -eq arg2 | True if arg1 equals arg2. |
arg1 -ne arg2 | True if arg1 is not equal to arg2. |
arg1 -lt arg2 | True if arg1 is less than arg2. |
arg1 -le arg2 | True if arg1 is less than or equal to arg2. |
arg1 -gt arg2 | True if arg1 is greater than arg2. |
arg1 -ge arg2 | True if arg1 is greater than or equal to arg2. |
Here are some simple examples. In the first instance we set the variable $X to 1 and then test to see if $X is equal to 1. In the second instance, X is set to 0 so the comparison is not true.
[david@testvm1 testdir1]$ X=1 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X equals 1
[david@testvm1 testdir1]$ X=0 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X does not equal 1
Try some more experiments on your own.
Miscellaneous operators
These miscellaneous operators allow us to see if a shell option is set or a shell variable has a value but it does not discover the value of the variable, just whether it has one.
Operator | Description |
-o optname | True if the shell option optname is enabled. See the list of options under the description of the -o option to the Bash set builtin in the Bash man page. |
-v varname | True if the shell variable varname is set (has been assigned a value). |
-R varname | True if the shell variable varname is set and is a name reference. |
You can also experiment on your own to try out these operators.
Expansions
Bash supports a number of types of expansions and substitutions which can be quite useful. According to the Bash man page, Bash has seven (7) forms of expansions. We will look at tilde (testdir) expansion, arithmetic expansion, and pathname expansion. Unofficially I also consider the dash (-) to be the 8th form of expansion.
Brace expansion
Brace expansion is a method to use for generating arbitrary strings. Let’s start with brace expansion because we will use this tool to create a large number of files to use in experiments with special pattern characters. Brace expansion can be used to generate lists of arbitrary strings and insert them into a specific location within an enclosing static string, or at either end of a static string. This may be hard to visualize so let’s just do it.
First let’s just see what a brace expansion does.
[david@testvm1 testdir1]$ echo {string1,string2,string3}
string1 string2 string3
Well, that is not very helpful, is it? But look what happens when we use it just a bit differently.
[david@testvm1 testdir1]$ echo "Hello "{David,Jen,Rikki,Jason}.
Hello David. Hello Jen. Hello Rikki. Hello Jason.
That looks like something we might be able to use because it can save a good deal of typing. Now try this.
[david@testvm1 testdir1]$ echo b{ed,olt,ar}s
beds bolts bars
I could go on but you get the idea. We’ll use expansions like this in Episode III
Tilde expansion
Arguably the most common expansion we run into is the tilde (~) expansion. When we use this in a command like cd ~/Documents, the Bash shell actually expands that shortcut to the full home directory of the user. The tilde uses the value in the $PWD environment variable but the tilde requires less typing than $PWD so all of us lazy sysadmins are very happy about that. The $PWD variable is always set to the home directory of the logged-in user.
[dboth@testvm1 ~]$ echo $PWD
/home/dboth
[dboth@testvm1 ~]$
Use these little Bash programs to explore tilde expansion.
[david@testvm1 testdir1]$ echo ~
/home/student
[david@testvm1 testdir1]$ echo ~/Documents
/home/student/Documents
[david@testvm1 testdir1]$ Var1=~/Documents ; echo $Var1 ; cd $Var1
/home/student/Documents
[david@testvm1 Documents]$
Dash expansion
The dash (-) is also a shortcut and expands to the value of the previous directory. It uses the value in the $OLDPWD environment variable. So let’s first look at that value and then use the dash to return to the previous directory.
[dboth@testvm1 Documents]$ echo $OLDPWD
/home/dboth/testdir1
[dboth@testvm1 Documents]$ cd -
/home/dboth/testdir1
[dboth@testvm1 testdir1]$
The dash (-) is also a shortcut and expands to the value of the previous directory. It uses the value in the $OLDPWD environment variable. So
Pathname expansion
Pathname expansion is fancy term for expansion of file globbing patterns using the characters ? and * into the full names of directories and files that match the pattern. File globbing means special pattern characters that allow us significant flexibility in matching file names, directories, and other strings when performing various actions. These special pattern characters allow matching single, multiple or specific characters in a string.
- ? – Matches only one of any character in the specified location within the string.
- * – Zero or more of any character in the specified location within the string.
In this case we apply this expansion to matching file and directory names. Let’s see how this works. Make your home directory the PWD and start with a plain listing. Of course the contents of my home directory will be different from yours.
[dboth@testvm1 testdir1]$ cd ; ls
Desktop dmesg3.txt file0.txt file5.txt Music testdir1 testfile1.txt testfile6.txt
development dmesg4.txt file1.txt file6.txt newfile.txt testdir6 testfile2.txt testfile7.txt
diskusage.txt dmesg.txt file2.txt file7.txt Pictures testdir7 testfile3.txt testfile8.txt
dmesg1.txt Documents file3.txt file8.txt Public testfile0.txt testfile4.txt testfile9.txt
dmesg2.txt Downloads file4.txt file9.txt Templates TestFile1 testfile5.txt Videos
[dboth@testvm1 ~]$
Now list the directories that start with “te”.
[dboth@testvm1 ~]$ ls te*
testfile0.txt testfile2.txt testfile4.txt testfile6.txt testfile8.txt
testfile1.txt testfile3.txt testfile5.txt testfile7.txt testfile9.txt
testdir1:
diskusage.txt dmesg2.txt dmesg4.txt file0.txt file2.txt file4.txt file6.txt file8.txt newfile.txt
dmesg1.txt dmesg3.txt dmesg.txt file1.txt file3.txt file5.txt file7.txt file9.txt testdir2
testdir6:
testdir7:
[dboth@testvm1 ~]$
Well that did not do exactly what we want. It listed all the files and directories in the home directory as well as the contents of the directories that begin with “te”. To list only the current directory and its contents we can use the -d option.
[dboth@testvm1 ~]$ ls -d te*
testdir1 testdir7 testfile1.txt testfile3.txt testfile5.txt testfile7.txt testfile9.txt
testdir6 testfile0.txt testfile2.txt testfile4.txt testfile6.txt testfile8.txt
[dboth@testvm1 ~]$
So what happens here – in both cases – is that the Bash shell expands the te* pattern into the names of the two directories and all the files that match the pattern. Any files and directories that match the pattern are expanded to their full names and displayed.
Command substitution
Command substitution is a form of expansion. Command substitution is a tool that allows the STDOUT data stream of one command to be used as the argument of another command, for example, as a list of items to be processed in a loop. The Bash man page says, “Command substitution allows the output of a command to replace the command name.” I find that to be accurate, if a bit obtuse.
There are two forms of this substitution, `command` and $(command). In the older form using back tics (`), a backslash (\) used in the command retains its literal meaning. However when used in the newer parenthetical form, the backslash takes on its meaning as a special character. Note also that the parenthetical form uses only single parentheses to open and close the command statement.
I frequently use this capability in command line programs and scripts where the results of one command can be used as an argument for another command.
Let’s start with a very simple example using both forms of this expansion. Ensure that testdir1 is the PWD.
[dboth@testvm1 testdir1]$ echo "Todays date is `date`"
Todays date is Mon Nov 20 10:41:43 AM EST 2023
[dboth@testvm1 testdir1]$ echo "Todays date is $(date)"
Todays date is Mon Nov 20 10:41:59 AM EST 2023
[dboth@testvm1 testdir1]$
The sequence command can be used to generate a sequence of numbers for use in Bash scripts and command line programs. This is another of those tools that I use frequently. Let’s start with some simple examples.
[dboth@testvm1 testdir1]$ seq 25
1
2
3
4
5
6
7
8
9
10
<SNIP>
22
23
24
25
[dboth@testvm1 testdir1]$
This stream won’t sort well so the -w option to the seq utility adds leading zeros to the numbers generated to that they are all the same width, i.e., number of digits regardless of the value. This makes it easier to sort them in numeric sequence.
[dboth@testvm1 testdir1]$ seq -w 50
01
02
03
04
05
06
07
08
09
10
11
12
<SNIP>
47
48
49
50
[dboth@testvm1 testdir1]$
Now we can do something a bit more useful like create a large number of empty files for testing.
[david@testvm1 testdir1]$ for I in $(seq -w 5000) ; do touch file-$I ; done ; ls | column | less
In this usage, the statement seq -w 5000
generates a list of numbers from 1 to 5000. By using command substitution as part of the for statement, the list of numbers is used by the for statement to generate the numerical part of the file names. The column statement causes the output to be displayed in columns so that it can be viewed more easily by humans.
Arithmetic expansion
Bash does perform integer math but it is rather cumbersome to do so, as you will soon see. The syntax for arithmetic expansion is $((arithmetic-expression))
using double parentheses to open and close the expression. Arithmetic expansion works like command substitution in a shell program or script – the value calculated from the expression replaces the expression for further evaluation by the shell.
Once again we will start with something simple.
[dboth@testvm1 testdir1]$ echo $((1+1))
2
[dboth@testvm1 testdir1]$ Var1=5 ; Var2=7 ; Var3=$((Var1*Var2)) ; echo "Var 3 = $Var3"
Var 3 = 35
[dboth@testvm1 testdir1]$
The following division results in zero because the result would be a decimal value of less than one.
[dboth@testvm1 testdir1]$ Var1=5 ; Var2=7 ; Var3=$((Var1/Var2)) ; echo "Var 3 = $Var3"
Var 3 = 0
[dboth@testvm1 testdir1]$
Here is a simple calculation that I do in a script or CLI program that tells me how much total virtual memory I have in a Linux host. The free command does not provide that data.
[dboth@testvm1 testdir1]$ RAM=`free | grep ^Mem | awk '{print $2}'` ; Swap=`free | grep ^Swap | awk '{print $2}'` ; echo "RAM = $RAM and Swap = $Swap" ; echo "Total Virtual memory is $((RAM+Swap))" ;
RAM = 8117080 and Swap = 8116220
Total Virtual memory is 16233300
[dboth@testvm1 testdir1]$
Note that I used the ` character to delimit the sections of code that were used for command substitution. I use Bash arithmetic expansion mostly for checking system resource amounts in a script and then choosing a program execution path based on the result.
Summary
In this second installment of our series on Bash as a programming language we explore the five types of Bash file, string, numeric, and miscellaneous logical operators that provide execution flow control logic; different types of shell expansions.
In the third article of this series, we will explore the use of loops for performing various types of iterative operations.
Series Articles
This list contains links to all eight articles in this series about Bash.
- Programming Bash #1 – Introducing a New Series
- Programming Bash #2: Getting Started
- Programming Bash #3: Logical Operators
- Programming Bash #4: Using Loops
- Programming Bash #5: Automation with Scripts
- Programming Bash #6: Creating a template
- Programming Bash #7: Bash Program Needs Help
- Programming Bash #8: Initialization and sanity testing