Programming Bash #2: Getting Started
Last Updated on December 29, 2023 by David Both
Bash as a programming language
We have all used Bash to issue commands. They are usually fairly simple and straightforward. But Bash can go beyond entering single commands. There are many times when SysAdmins create simple command line programs to perform a series of tasks. They are a common tool and can save a lot of time and effort.
My personal objective when writing CLI programs is to save time and to “be the lazy SysAdmin.” CLI programs allow me to accomplish this by listing several commands in a specific sequence so that they will execute one after another. As a result I do not need to watch the progress of one command and, when it has finished, type in the next command. I can go do other things and not be concerned about having to continually monitor the progress of each command.
Definition of a program
Let’s define what we mean by a program. The Free On-line Dictionary of Computing1 (FOLDOC) defines a program as, “The instructions executed by a computer, as opposed to the physical device on which they run.” Other sources such as Princeton University’s WordNet2 define a program as, “… a sequence of instructions that a computer can interpret and execute…”. Wikipedia also has a good article about computer programs3.
Based on these definitions a program can consist of one or more instructions that perform a specific related task. A computer program instruction is also called a program statement. For SysAdmins, a program is usually a sequence of shell commands. All of the shells available for Linux, at least the ones with which I am familiar, have at least some basic form of programming capability and Bash, the default shell for most Linux distributions, is no exception. This chapter uses Bash because it is so ubiquitous. You may already prefer or later learn about and come to prefer a different shell. If so, the programming concepts will be the same though the constructs and syntax may differ somewhat. Some shells may support some features that others do not. But they all provide some programming capability.
These shell programs can be stored in a file for repeated use, or they may be simply created on an ad hoc basis at the command line as needed. In this chapter we will start working directly at the command line. In Chapter 10, “Automation with Bash Scripts,” we will discuss storing our simple programs in files for sharing and re-use, and more complex and lengthy programs.
Simple CLI programs
The simplest command line programs are one or two consecutive program statements, which may be related or not, that are entered on the command line before the Enter key is pressed. For example the second statement, if there is one, may be dependent upon the actions of the first but it does not need to be.
There is also one bit of syntactical punctuation that needs to be clearly stated. When entering a single command on the command line, pressing the Enter key terminates the command with an implicit semicolon (;). When used in a CLI shell program entered as a single line on the command line, the semicolon must be used to terminate each statement and separate it from the next one. The last statement in a CLI shell program can use an explicit or implicit semicolon.
Some basic syntax
Let’s look at a couple examples which will clarify this syntax. The following program consists of a single command with an explicit terminator.
[dboth@testvm1 ~]$ echo "Hello world." ;
Hello world.
That may not seem like much of a program, but it is the same first program I have encountered with every new programming language I have ever learned. The syntax may be a bit different for each language but the result is the same.
Let’s expand on this trivial but ubiquitous program a little. Your results will be different from mine. You may have only the default directories and files in the account home directory that are created the first time you login to an account via the GUI desktop.
[dboth@testvm1 ~]$ echo "My home directory." ; ls ;
My home directory.
chapter25 TestFile1.Linux dmesg2.txt Downloads newfile.txt softlink1 testdir6
chapter26 TestFile1.mac dmesg3.txt file005 Pictures Templates testdir
TestFile1 Desktop dmesg.txt link3 Public testdir Videos
TestFile1.dos dmesg1.txt Documents Music random.txt testdir1
That makes a bit more sense. The results are related but the individual program statements are independent of each other. Notice that I like spaces before and after the semicolon which makes the code a bit easier to read. Try this little CLI program again without an explicit semicolon at the end.
[dboth@testvm1 ~]$ echo "My home directory." ; ls
There is no difference in the output data.
Something about variables
Like all programming languages the Bash shell can deal with variables. A variable is a symbolic name that refers to a specific location in memory which contains a value of some sort. The value of a variable is changeable, i.e., it is variable.
Bash does not type variables like C and related languages, defining them as integers, floating point, or string types. In Bash, all variables are strings. A string that is an integer can be used in integer arithmetic which is all that Bash has the capability of doing. If more complex maths are required the bc command can be used in CLI programs and scripts.
Variables are assigned values and can then be used to refer to this values in CLI programs and scripts. The value of a variable is set using its name but not preceded by a $ sign. The assignment VAR=10 sets the value of the variable VAR to 10. To print the value of the variable we can use the statement, echo $VAR. Let’s start with text variables, i.e., non-numeric.
Bash variables become part of the shell environment until they are unset.
Let’s check the initial value of a variable that has not been assigned. It should be null. Then we will assign a value to the variable and print it to verify its value. We will do all of this in a single CLI program.
Note: The syntax of variable assignment is very strict. There must be no spaces on either side of the equal (=) sign in the assignment statement.
[dboth@testvm1 ~]$ echo $MyVar ; MyVar="Hello World" ; echo $MyVar ;
Hello World
[dboth@testvm1 ~]$
The empty line indicates that the initial value of MyVar is null. Changing the value of a variable is the same as setting it in the first place. In this example we can see both the original and the new value.
Bash can perform integer arithmetic calculations which is useful for calculating a reference to the location of an element in an array or simple math problems. It is not suitable for scientific computing or anything that requires decimals such as financial calculations. There are much better tools for those types of calculations.
Let’s look at a simple calculation.
[dboth@testvm1 ~]$ Var1="7" ; Var2="9" ; echo "Result = $((Var1*Var2))"
Result = 63
What happens when we perform a math operation that results in a floating point number?
[dboth@testvm1 ~]$ Var1="7" ; Var2="9" ; echo "Result = $((Var1/Var2))"
Result = 0
[dboth@testvm1 ~]$ Var1="7" ; Var2="9" ; echo "Result = $((Var2/Var1))"
Result = 1
[dboth@testvm1 ~]$
The result is the nearest integer. Be sure to notice that the calculation was performed as part of the echo statement
Control operators
Shell control operators are one of the syntactical operators that allow us to easily create some interesting command line programs. We have seen that the simplest form of CLI program is just stringing several commands together in a sequence on the command line.
command1 ; command2 ; command3 ; command4 ; . . . ; etc. ;
Those commands will all run without a problem so long as no errors occur. But what happens when an error occurs? We can anticipate and allow for errors using the && and || built-in bash control operators. These two control operators provide us with some flow control and enable us to alter the sequence of code execution. The semicolon is also considered to be a bash control operator as is the newline character.
The && operator simply says that if command1 is successful then run command2. If command1 fails for any reason, then command2 is skipped. That syntax looks like this.
command1 && command2
Now lets look at some commands that will create a new directory and – if successful – make it the PWD. Ensure that your home directrory (~) is the Present Working Directory (PWD). We are going to try this first in a directory for which we do not have access, /root.
[david@testvm1 ~]$ Dir=/root/testdir ; mkdir $Dir/ && cd $Dir
mkdir: cannot create directory '/root/testdir/': Permission denied
[david@testvm1 ~]$
The error seen in the preceding example was emitted by the mkdir command. We did not receive an error indicating that the file could not be created because creation of the directory failed. The && control operator sensed the non-zero return code so the touch command was skipped. Using the && control operator prevents the touch command from running because there was an error in creating the directory. This type of command line program flow control can prevent errors from compounding and making a real mess of things. But let’s get a little more complicated.
The || control operator allows us to add another program statement that executes when the initial program statement returns a code greater than zero. The basic syntax looks like this.
command1 || command2
This syntax reads, If command1 fails, execute command2. That implies that if command1 succeeds, command2 is skipped. Let’s try this with our attempt to create a new directory.
[david@testvm1 ~]$ Dir=/root/testdir ; mkdir $Dir || echo "$Dir was not created."
mkdir: cannot create directory '/root/testdir': Permission denied
/root/testdir was not created.
[david@testvm1 ~]$
This is exactly what we expected. Because the new directory could not be created, the first command failed which resulted in execution of the second command.
Combining these two operators gives us the best of both. Our control operator syntax using some flow control now takes this general form when we use the && and || control operators.
preceding commands ; command1 && command2 || command3 ; following commands
This syntax can be stated like so: if command1 exits with a return code of 0 then execute command2, otherwise execute command3. Let’s try it.
[david@testvm1 ~]$ Dir=/root/testdir ; mkdir $Dir && cd $Dir || echo "$Dir was not created."
mkdir: cannot create directory '/root/testdir': Permission denied
/root/testdir was not created.
[david@testvm1 ~]$
Now try this last command again using our home directory instead of the /root directory. We will have permission to create this directory.
[david@testvm1 ~]$ Dir=~/testdir ; mkdir $Dir && cd $Dir || echo "$Dir was not created."
[david@testvm1 testdir]$
The control operatror syntax like, command1 && command2, works because every command sends a return code (RC) to the shell that indicates whether it completed successfully or whether there was some type of failure during execution. By convention, a return code of zero (0) indicates success and any positive number indicates some type of failure. Some of the tools we use as SysAdmins return only a one (1) to indicate a failure, but many can return other codes as well to further indicate the type of failure that occurred.
The bash shell has a variable, $?, which contains the return code from the last command. This return code can be checked very easily by a script, the next command in a list of commands, or even us SysAdmins. Let’s start by looking at return codes. We can run a simple command and then immediately check the return code. The return code will always be for the last command that was run before we look at it.
[dboth@testvm1 ~]$ ll ; echo "RC = $?"
total 832
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Desktop
drwxr-xr-x. 3 root root 4096 Aug 5 16:02 development
-rw-r--r-- 1 dboth dboth 685 Nov 18 17:59 diskusage.txt
-rw-r--r-- 1 dboth dboth 50831 Nov 18 18:00 dmesg1.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 17:58 dmesg2.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 17:58 dmesg3.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 17:58 dmesg4.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 17:58 dmesg.txt
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Documents
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Downloads
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file0.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file1.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file2.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file3.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file4.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file5.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file6.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file7.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file8.txt
-rw-r--r-- 1 dboth dboth 50819 Nov 18 18:01 file9.txt
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Music
-rw-r--r-- 1 dboth dboth 0 Nov 18 17:59 newfile.txt
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Pictures
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Public
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Templates
drwxr-xr-x 3 dboth dboth 4096 Nov 18 17:59 testdir1
drwxr-xr-x 2 dboth dboth 4096 Nov 18 17:59 testdir6
drwxr-xr-x 2 dboth dboth 4096 Nov 18 17:59 testdir7
drwxr-xr-x. 2 dboth dboth 4096 Jul 4 10:13 Videos
RC = 0
[dboth@testvm1 ~]$
The return code (RC) in this case is zero (0) which means the command completed successfully. Now try the same command on a directory for which we do not have permissions, root’s home directory.
[dboth@testvm1 ~]$ ll /root ; echo "RC = $?"
ls: cannot open directory '/root': Permission denied
RC = 2
[dboth@testvm1 ~]$
In this case the return code is 2 which specifically means that permission was denied for a non-root user to access a directory to which the user is not permitted access. The control operators use these return codes to enable us to alter the sequence of program execution.
Summary
In this article we have looked at Bash as a programming language and explored its basic syntax as well as some basic tools. We have seen how to print data to STDOUT, and how to use variables, and control operators. In the next article in this series we look at some of the many Bash logical operators that enable us to control the flow of instruction execution.
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