Introduction to Bash Shell Scripting
Once you are really at ease working on the command line, you’ll want more. You’ve already learned how to combine commands using pipes, but if you truly want to get the best out of your commands, there is much more you can do. In this chapter, you’ll get an introduction to the possibilities of Bash shell scripting, which helps you accomplish difficult tasks in an easy way. Once you understand shell scripting with Bash, you’ll be able to automate many tasks and, thus, do your work much faster and more efficiently.
The following topics are discussed:
Getting Started: Shell Scripting Fundamentals
A shell script is a text file that contains a sequence of commands. So, basically, anything that can run a bunch of commands can be considered a shell script. Nevertheless, there are some basic recommendations that ensure that you’ll create decent shell scripts. These are scripts that will not only perform the task you’ve written them for but also be readable by others and organized in an efficient manner.
At some point in time, you’ll be glad of the habit of writing readable shell scripts. Especially if your scripts get longer and longer, you will notice that when a script does not meet the basic requirements of readability, even you risk not being able to understand what it is doing.
Elements of a Good Shell Script
When writing a script, make sure that you heed the following recommendations:
In Exercise 14-1, you’ll create a shell script that meets all of the basic requirements.
EXERCISE 14-1. CREATING A BASIC SHELL SCRIPT
#!/bin/bash
# this is the hello script
# run it by typing ./hello in the directory where you've found it
clear
echo hello world
exit 0
You have just created your first script. In this script, you’ve used some elements that you’ll use in many future shell scripts that you’ll write.
Let’s talk about the name of the script first. You’ll be amazed at how many commands exist on your computer. So, you have to make sure that the name of your script is unique. For example, many people like to give the name test to their first script. Unfortunately, there’s an existing command with the name test (see later in this chapter). If your script has the same name as an existing command, the existing command will be executed, and not your script (unless you prefix the name of the script with ./). So, make sure that the name of your script is not already in use. You can find out if the name already exists by using the which command. For example, if you want to use the name hello and want to be sure that it’s not already in use, type which hello. Listing 14-1 shows the result of this command.
Listing 14-1. Use which to Find Out If the Name of Your Script Is Not Already in Use
nuuk:~ # which hello
which: no hello in
(/sbin:/usr/sbin:/usr/local/sbin:/opt/gnome/sbin:/root/bin:/usr/local/bin:/usr/bin:/usr/X11R6/bin:/bin
:/usr/games:/opt/gnome/bin:/opt/kde3/bin:/usr/lib/mit/bin:/usr/lib/mit/sbin)
Let’s have a look at the content of the script you’ve created in Exercise 14-1. In the first line of the script, you can find the shebang. This scripting element tells the parent shell which subshell should be used to run this script. This may sound rather cryptic, but it is not too hard to understand.
If you run a command from a shell, the command becomes the child process of the shell. You can verify that by using a command such as ps fax or pstree. Likewise, if you run a script from the shell, the script becomes a child process of the shell. This makes scripts portable and ensures that even if you’re using korn shell as your current shell, a Bash script can still be executed.
To tell your current shell which subshell should be executed when running the script, include the shebang. The shebang always starts with #! and is followed by the name of the subshell that should execute the script. In Exercise 14-1, you used /bin/bash as the subshell. By doing this, you can run this script from any shell, even if it contains items that are specific only to Bash. In the example from Listing 14-1, you could have done without the shebang, because there is not really any Bash-specific code in it, but it’s a good habit to include a shebang in any script you like.
You will notice that not all scripts include a shebang, and in many cases, even if your script doesn’t include a shebang, it will still run. The shell just executes the script using the same shell for the subshell process. However, if a user who uses a shell other than /bin/bash tries to run a script without a shebang, it will probably fail. You can avoid this by always including a shebang. So, just make sure it’s in there!
The second part in the sample script in Exercise 14-1 includes two lines of comment. As you can guess, these comment lines explain to the user what the purpose of the script is and how to use it. There’s only one rule regarding the comment lines: they should be clear and explain what’s happening. A comment line always starts with a #, followed by anything.
Note You may ask why the shebang, which also starts with a #, is not interpreted as a comment. That is because of its position and the fact that it is immediately followed by an exclamation point. This combination at the very start of a script tells the shell that it’s not a comment but a shebang.
Following the comment lines in the script you have created in Exercise 14-1, there is the body of the script itself, which contains the code that the script should execute. In the example, the code consists of two simple commands: first, the screen is cleared, and next, the text “hello world” is echoed to the screen.
As the last part of the script, the command exit 0 is used. It is good habit to use the exit command in all your scripts. This command exits the script and next tells the parent shell how the script has executed. If the parent shell reads exit 0, it knows the script executed successfully. If it encounters anything other than exit 0, it knows there is a problem. In more complex scripts, you could even start working with different exit codes. Use exit 1 as a generic error message, and exit 2 etcetera to specify that a specific condition was not met. When applying conditional loops later, you’ll see that it may be very useful to work with exit codes. From the parent shell, you can check the exit status of the last command by using the command echo $?.
Executing the Script
Now that your first shell script is written, it’s time to execute it. There are different ways of doing this:
Making the Script Executable
The most common way to run the shell script is by making it executable. To do this with the hello script from the sample in Exercise 14-1, you would use the following command:
chmod +x hello
After making the script executable, you can run it, just like any other normal command. The only limitation is the exact location in the directory structure where your script is. If it is in the search path, you can run it by typing any command. If it is not in the search path, you have to run it from the exact directory where it is. That means that if linda created a script with the name hello that is in /home/linda, she has to run it using the command /home/linda/hello. Alternatively, if she is already in /home/linda, she could use ./hello to run the script. In the latter example, the dot and the slash tell the shell to run the command from the current directory.
Tip Not sure if a directory is in the path or not? Use echo $PATH to find out. If it’s not, you can add a directory to the path by redefining it. When defining it again, you’ll mention the new directory, followed by a call to the old path variable. For example, to add the directory /something to the PATH, you would use PATH=$PATH:/something.
Running the Script As an Argument of the bash Command
The second way to run a script is by specifying its name as an argument of the bash command. For instance, our example script hello would run by using the command bash hello. The advantage of running the script in this way is that there is no need to make it executable first.
When running the script this way, make sure that you are using a complete path to where the script is. It has to be in the current directory, or you will have to use a complete path to the directory where it is. That means that if the script is /home/linda/hello, and your current directory is /tmp, you should run it using the command bash /home/linda/hello.
Sourcing the Script
The third way of running the script is completely different. You can source the script. By sourcing a script, you don’t run it as a subshell, but you are including it in the current shell. This may be useful if the script contains variables that you want to be active in the current shell (this happens often in the scripts that are executed when you boot your computer).
If you source a script, you cannot use the exit command. Whereas in normal scripts the exit command is used to close the subshell, if used from a script that is sourced, it would close the parent shell, and normally, that is not what you want.
There are two ways to source a script: you can use the .command (yes, that is just a dot!), or you can use the source command. In Exercise 14-2, you will create two scripts that use sourcing. In the script, you are defining a variable that is used later. In the next section of this chapter you’ll read more about using variables in scripts.
EXERCISE 14-2. USING SOURCING
COLOR=blue
#!/bin/bash
# example script that demonstrates sourcing
. ~/bin/color
echo the color is $COLOR
Working with Variables and Input
Variables are essential to efficient shell scripting. The purpose of using variables is to have scripts work with changing data. The value of a variable depends on how it is used. You can have your script get the variable itself, for example, by executing a command, by making a calculation, by specifying it as a command-line argument for the script, or by modifying some text string. You can also set it yourself, as you’ve seen in the sample script from Exercise 14-2. In this section, you’ll learn all the basics about variables.
Understanding Variables
A variable is a value that you define somewhere specific and use in a flexible way later. You can do that in a script, but you don’t have to. You can define a variable in the shell as well. To define a variable, you use VARNAME=value. To get the value of a variable later on, you can call its value by using the echo command. Listing 14-2 gives an example of how a variable is set on the command line and how its value is used in the next command.
Listing 14-2. Setting and Using a Variable
nuuk:~ # HAPPY=yes
nuuk:~ # echo $HAPPY
yes
Note The method described here works for the bash command. Not every shell supports this. For instance, on TCSH, you have to use the set command to define a variable. For example, use set happy=yes to give the value yes to the variable happy.
Variables play a very important role on Linux. When booting, lots of variables are defined and used later when you work with your computer. For example, the name of your computer is in a variable; the name of the user account you logged in with is in a variable, as is the search path.
When starting a computer, the environment, which contains all of these variables, is set for users. You can use the env command to get a complete list of all the variables that are set for your computer.
When defining variables, it is a good idea to use uppercase. If your script gets longer, using variables in uppercase makes the script more readable. This is, however, in no way a requirement. An environment variable can very well be in lowercase.
The advantage of using variables in shell scripts is that you can use them in different ways to treat dynamic data. Here are some examples:
In many scripts, all the variables are defined in the beginning of the script. This makes administration easier, because all the flexible content is easily accessible and referred to later in the script. Variables can also be stored in files, as you have seen in Exercise 14-2. Let’s have a look at the example in Listing 14-3.
Listing 14-3. Understanding the Use of Variables
#!/bin/bash
#
# dirscript
#
# Script that creates a directory with a certain name
# next sets $USER and $GROUP as the owners of the directory
# and finally changes the permission mode to 770
DIRECTORY=/blah
USER=linda
GROUP=sales
mkdir $DIRECTORY
chown $USER $DIRECTORY
chgrp $GROUP $DIRECTORY
chmod 770 $DIRECTORY
exit 0
As you can see, after the comment lines, this script starts by defining all the variables that are used. I’ve specified them in all uppercase letters, because they’re more readable. In the second part of the script, the variables are referred to by preceding their name with a $ sign.
You will notice that many scripts work in this way. Apart from defining variables in this static way, you can also define variables dynamically, by using command substitution. You’ll read more about this later in this chapter.
Variables, Subshells, and Sourcing
When defining variables, you should be aware that a variable is defined for the current shell only. That means that if from the current shell you start a subshell, the variable won’t be there anymore. And if in a subshell you define a variable, it won’t be there anymore once you’ve quit the subshell and returned to the parent shell. Listing 14-4 shows how this works.
Listing 14-4. Variables Are Local to the Shell Where They Are Defined
nuuk:~/bin # HAPPY=yes
nuuk:~/bin # echo $HAPPY
yes
nuuk:~/bin # bash
nuuk:~/bin # echo $HAPPY
nuuk:~/bin # exit
exit
nuuk:~/bin # echo $HAPPY
yes
nuuk:~/bin #
In the preceding listing, I’ve defined a variable with the name HAPPY. Next, you can see that its value is correctly echoed. In the third command, a subshell is started, and as you can see, when asking for the value of the variable HAPPY in this subshell, it isn’t there, because it simply doesn’t exist. But when the subshell is closed by using the exit command, we’re back in the parent shell, where the variable still exists.
In some cases, you may want to set a variable that is present in all subshells as well. If this is the case, you can define it by using the export command. For instance, the command export HAPPY=yes would define the variable HAPPY and make sure that it is available in all subshells from the current shell on, until you next reboot the computer. There is, however, no way to define a variable and make that available in the parent shells in this way.
In Listing 14-5, you can see the same commands as used in Listing 14-4, but now with the value of the variable being exported.
Listing 14-5. By Exporting a Variable You Can Make It Available in Subshells As Well
nuuk:~/bin # export HAPPY=yes
nuuk:~/bin # echo $HAPPY
yes
nuuk:~/bin # bash
nuuk:~/bin # echo $HAPPY
yes
nuuk:~/bin # exit
exit
nuuk:~/bin # echo $HAPPY
yes
nuuk:~/bin #
Working with Script Arguments
In the preceding section, you have learned how you can define variables. Up to now, you’ve seen how to create a variable in a static way. In this subsection, you’ll learn how to provide values for your variables in a dynamic way, by specifying them as an argument for the script when running the script on the command line.
Using Script Arguments
When running a script, you can specify arguments to the script on the command line. Consider the script dirscript that you’ve seen in sample Listing 14-3. You could run it with an argument on the command line as well, as in the following example:
dirscript /blah
Now wouldn’t it be nice if in the script, you could do something with its argument /blah? The good news is that you can. You can refer to the first argument that was used when launching the script by using $1 in the script. The second argument is $2, and so on, up to $9. You can also use $0 to refer to the name of the script itself. In Exercise 14-3, you’ll create a script that works with arguments and see how it works for yourself.
EXERCISE 14-3. CREATING A SCRIPT THAT WORKS WITH ARGUMENTS
In this exercise, you’ll create a script that works with arguments. Type the following code and execute it, to find out what it does. Save the script, using the name ~/bin/argscript. First, run it without any arguments. Next, see what happens if you put one or more arguments after the name of the script.
#!/bin/bash
#
# argscript
#
# Script that shows how arguments are used
ARG1=$1
ARG2=$2
ARG3=$3
SCRIPTNAME=$0
echo The name of this script is $SCRIPTNAME
echo The first argument used is $ARG1
echo The second argument used is $ARG2
echo The third argument used is $ARG3
exit 0
In Exercise 14-4, you’ll make a rewrite of the script dirscript that you’ve used before. You’ll rewrite it to use arguments. This changes dirscript from a rather static script that can create only one directory to a dynamic script that can create any directory and assign any user and any group as an owner of that directory.
EXERCISE 14-4. REFERRING TO COMMAND-LINE ARGUMENTS IN A SCRIPT
The following script shows a rewrite of the dirscript that you’ve used before. In this new version, the script works with arguments instead of fixed variables, which makes it a lot more flexible!
#!/bin/bash
#
# dirscript
#
# Script that creates a directory with a certain name
# next sets $USER and $GROUP as the owners of the directory
# and finally changes the permission mode to 770
# Provide the directory name first, followed by the username and
# finally the groupname.
DIRECTORY=$1
USER=$2
GROUP=$3
mkdir $DIRECTORY
chown $USER $DIRECTORY
chgrp $GROUP $DIRECTORY
chmod 770 $DIRECTORY
exit 0
To execute the script from Exercise 14-4, you would use a command, as in the next sample code line:
dirscript somedir denise sales
This line shows you how the dirscript has been made more flexible now, but at the same time, it also shows you the most important disadvantage: it has become less obvious as well. You can imagine that for a user it is very easy to mix up the right order of the arguments and type dirscript kylie sales /somedir instead. So it becomes important to provide good help information on how to run this script. Do this by including comments at the beginning of the script. That explains to the user what exactly you are expecting.
Counting the Number of Script Arguments
On some occasions, you’ll want to check the number of arguments that is provided with a script. This is useful if you expect a certain number of arguments and want to make sure that the required amount of arguments is present before running the script. To count the number of arguments provided with a script, you can use $#. Used all by itself, $# doesn’t really make sense. Combined with an if statement (about which you’ll read more later in this chapter), it does make sense. You could, for example, use it to show a help message, if the user hasn’t provided the correct amount of arguments. In Exercise 14-5, you see the contents of the script countargs, in which $# is used. Directly following the code of the script, you also see a sample running of it.
EXERCISE 14-5. COUNTING ARGUMENTS
One useful technique to check whether the user has provided the expected number of arguments is to count these arguments. In this exercise, you’ll write a script that does just that.
#!/bin/bash
#
# countargs
# sample script that shows how many arguments were used
echo the number of arguments is $#
exit 0
If you run the script from Exercise 14-5 with a number of arguments, it will show you how many arguments it has seen. The following code listing shows what to expect:
nuuk:~/bin # ./countargs a b c d e
the number of arguments is 5
nuuk:~/bin #.
Referring to All Script Arguments
So far, you’ve seen that a script can work with a fixed number of arguments. The script that you’ve created in Exercise 14-4 is hard-coded to evaluate arguments as $1, $2, and so on. But what if the number of arguments is not known beforehand? In that case, you can use $@ in your script. Let’s show how this is used by creating a small for loop.
A for loop can be used to test all elements in a string of characters. Listing 14-6 shows how a for loop and $@ are used to evaluate all arguments that were used when starting a script.
Listing 14-6. Evaluating Arguments
#!/bin/bash
# showargs
# this script shows all arguments used when starting the script
echo the arguments are $@
for i in $@
do
echo $i
done
exit 0
When running this script, you can see that an echo command is executed for every single argument. The for loop takes care of that. You can read the part for i in as “for every element in. . .”. So, it processes all variables and starts an operation for each of them. When looping through the list of arguments, the argument is stored in the variable i, which is used in the command echo $i, which is executed for every argument that was encountered.
Prompting for Input
Another way to get user data is just to ask for it. To do this, you can use read in the script. When using read, the script waits for user input and puts that in a variable. In Exercise 14-6, you are going to create a simple script that first asks the input and then shows the input that was provided by echoing the value of the variable. Directly following the sample code, you can see also what happens when you run the script.
EXERCISE 14-6. PROMPTING FOR INPUT WITH READ
Create the script ~/bin/askinput containing the following contents:
#!/bin/bash
#
# askinput
# ask user to enter some text and then display it
echo Enter some text
read SOMETEXT
echo -e "You have entered the following text: $SOMETEXT"
exit 0
Now let’s see what happens if you run the script code from the previous exercise.
nuuk:~/bin # ./askinput
Enter some text
hi there
You have entered the following text: hi there
nuuk:~/bin #
As you can see, the script starts with an echo line that explains to the user what it expects the user to do. Next, in the line read SOMETEXT, it will stop to allow the user to enter some text. This text is stored in the variable SOMETEXT. In the following line, the echo command is used to show the current value of SOMETEXT. As you see in this sample script, I’ve used echo with the option -e. This option allows you to use some special formatting characters, in this case, the formatting character , which enters a tab in the text. Using formatting such as this, you can ensure that the result is displayed in a nice manner.
As you can see, in the line that has the command echo -e, the text that the script needs to be echoed is between double quotes. That is to prevent the shell from interpreting the special character before echo does. Again, if you want to make sure that the shell does not interpret special characters such as this, put the string between double quotes.
In the previous script, two new items have been introduced: formatting characters and escaping. By escaping characters, you can make sure they are not interpreted by the shell. This is the difference between echo and echo " ". In the former, the is treated as a special character, with the result that only the letter t is displayed. In the latter, double quotes are used to tell the shell not to interpret anything that is between the double quotes; hence, it shows . But in the script, we’ve used echo -e, which tells the script to understand as a formatting character.
When running shell scripts, you can use formatting. We’ve done that by using the formatting character with the command echo -e. This is one of the special characters that you can use in the shell, and this one tells the shell to display a tab. But to make sure that it is not interpreted by the shell when it first parses the script (which would result in the shell just displaying a t), you have to put any of these special formatting characters between double quotes. Let’s have a look at how this works, in Listing 14-7.
Listing 14-7. Escaping and Special Characters
SYD:~ # echo
t
SYD:~ # echo " "
SYD:~ # echo -e
t
SYD:~ # echo -e " "
SYD:~ #
When using echo –e, you can use the following special characters: