Introduction to Bash Shell Scripting
Once you really get to be at ease working on the command line, you’ll want to do more than what the previous chapters have taught you. You’ve already learned how to combine commands using piping, but if you really 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 really is the command line on steroids; piping and redirection just is not enough if you need to do really complex tasks. As soon as you really understand shell scripting, you’ll be able to automate many tasks, and thus do your work at least twice as fast as you used to do it.
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, some rules exist for making sure that you create decent shell scripts, scripts that will not only do the task you’ve written them for, but also be readable by others. At some point in time, you’ll be happy with the habit of writing readable shell scripts. As your scripts get longer and longer, you will notice that if a script does not meet the basic requirements of readability, even you yourself won’t be able to understand what it is doing.
Elements of a Good Shell Script
When writing a script, make sure that you meet the following requirements:
Let’s start with an example script (see Listing 14-1).
Let’s talk about the name of the script first: you’ll be amazed how many commands already exist on your computer. So you have to make sure that the name of your script is unique. For instance, many people like to give the name test to their first script. Unfortunately, there’s already an existing command with that name (see the section “Using Control Structures” later in this chapter). If your script has the same name as an existing command, the existing command will be executed, not your script (unless you prefix the name of the script with ./). So make sure that the name of your script is not in use already. You can find out whether a name already exists by using the which command. For instance, if you want to use the name hello and want to be sure that it’s not in use already, type which hello. Listing 14-2 shows the result of this command.
In the first line of the script is the shebang. This scripting element tells the shell from which this script is executed which subshell should be executed to run this script. This may sound rather cryptic, but is not too hard to understand. If you run a command from a shell, the command becomes the child process of the shell; the pstree command will show you that perfectly. If you run a script from the shell, the script becomes a child process of the shell. This means that it is by no means necessary to run the same shell as your current shell to run the script. To tell your current shell which subshell should be executed when running the script, include the shebang. As mentioned previously, the shebang always starts with #! and is followed by the name of the subshell that should execute the script. In Listing 14-1, I’ve used /bin/bash as the subshell, but you can use any other shell if you’d 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. 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.
The second part of the example script in Listing 14-1 are two lines of comment. As you can guess, these command lines explain to the user what the purpose of the script is and how to use it. There’s only one rule about 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 mark. 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 is the body of the script itself, which contains the code that the script should execute. In the example from Listing 14-1, the code consists of two simple commands: the first clears the screen, and the second echoes the text “hello world” to the screen.
The last part of the script is the command exit 0. It is a 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 was 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 , and so forth, to specify that a specific condition was not met. When applying conditional loops later (see the section “Using Control Structures” later in this chapter), you’ll see that it may be very useful to work with exit codes.
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 a shell script is by making it executable. To do this with the hello script from the example in Listing 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 just any command. If it is not in the search path, you have to run it from the exact directory where it is. This 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 whether 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 instance, 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 option for running a script is to specify its name as the 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. Make sure that you are using a complete reference to the location where the script is when running it this way; it has to be in the current directory, or you have to use a complete reference to the directory where it is. This means that if the script is /home/linda/hello, and your current directory is /tmp, you should run it using the following command:
bash /home/linda/hello
Sourcing the Script
The third way of running the script is rather 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). Some problems may occur as well. For instance, if you use the exit command in a script that is sourced, it closes the current shell. Remember, the exit command exits the current script. In fact, it doesn’t exit the script itself, but tells the executing shell that the script is over and it has to return to its parent shell. Therefore, you don’t want to source scripts that contain the exit command. There are two ways to source a script. The next two lines show how to source a script that has the name settings:
. settings
source settings
It doesn’t really matter which one you use, as both are equivalent. When discussing variables in the next section, I’ll give you some more examples of why sourcing may be a very useful technique.
Working with Variables and Input
What makes a script so flexible is the use of variables. A variable is a value you get from somewhere that will be dynamic. The value of a variable normally depends on the circumstances. You can have your script get the variable itself, for instance, 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. In this section, you’ll learn all there is to know about variables.
A variable is a value that you define somewhere and use in a flexible way later. You can do this in a script, but you don’t have to, as 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 call its value by using the echo command. Listing 14-3 gives an example of how a variable is set on the command line and how its value is used in the next command.
Note The method described here works for the Bash and Dash shells. Not every shell supports this method, however. For instance, on tcsh, you need to use the set command to define a variable: set happy=yes gives the value yes to the variable happy.
Variables play a very important role on your computer. When booting, lots of variables are defined and used later when you work with your computer. For instance, the name of your computer is in a variable, the name of the user account you logged in with is in a variable, and the search path is in a variable as well. These are the shell variables, the so-called environment variables you get automatically when logging in to the shell. As discussed earlier, you can use the env command to get a complete list of all the variables that are set for your computer. You will notice that most environment variables are in uppercase. However, this is in no way a requirement; an environment variable can be in lowercase as well.
The advantage of using variables in shell scripts is that you can use them in three ways:
When reading some of the scripts that are used in your computer’s boot procedure, you will notice that the beginning of the script features a list of variables that are referred to several times later in the script. Let’s have a look at the somewhat silly example in Listing 14-4.
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, because it makes it a lot easier to recognize the variables when reading a longer script. In the second part of the script, the variables are referred to by typing in their names with a $ sign in front of each.
You will notice that quite a few scripts work in this way. There is a disadvantage though: it is a rather static way of working with variables. If you want a more dynamic way to work with variables, you can specify them as arguments to the script when executing it on the command line, for instance.
Variables, Subshells, and Sourcing
When defining variables, you should be aware that a variable is defined for the current shell only. This means that if you start a subshell from the current shell, the variable won’t be there. And if you define a variable in a subshell, it won’t be there anymore once you’ve quit the subshell and returned to the parent shell. Listing 14-5 shows how this works.
In Listing 14-5, I’ve defined a variable with the name HAPPY, and next 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.
Now 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 following command 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. However, there is no similar way to define a variable and make that available in the parent shells.
export HAPPY=yes
Note Make sure that you include the definition of variables in /etc/profile so that the new variable will also be available after a reboot.
Listing 14-6 shows the same commands as used in Listing 14-5, but now with the value of the variable being exported.
So that’s what you have to do to define variables that are available in subshells as well.
A technique you will see often as well that is related to variables is the sourcing of a file that contains variables. The idea is that somewhere on your computer you keep a common file that contains variables. For instance, consider the example file vars that you see in Listing 14-7.
The main advantage of putting all variables in one file is that you can make them available in other shells as well by sourcing them. To do this with the example file from Listing 14-7, you would use the following command (assuming that the name of the variable file is vars):
. vars
Note . vars is not the same as ./vars. With . vars, you include the contents of vars in the current shell. With ./vars, you run vars from the current shell. The former doesn’t start a subshell, whereas the latter does.
In Listing 14-8, you can see how sourcing is used to include variables from a generic configuration file in the current shell. In this example, I’ve used sourcing for the current shell, but the technique is also quite commonly used to include common variables in a script.
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 section, 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.
When running a script, you can specify arguments to the script on the command line. Consider the script dirscript that you’ve seen previously in Listing 14-4. 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 the argument /blah that is specified in the script? 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 by using $2, and so on, up to $9. You can also use $0 to refer to the name of the script itself. The example script in Listing 14-9 shows how it works.
The example code in Listing 14-10 shows how dirscript is rewritten to work with an argument that is specified on the command line. This changes dirscript from a rather static script that can create one directory only to a very dynamic script that can create any directory and assign any user and any group as the owner to that directory.
To execute the script from Listing 14-10, you would use a command as in this example:
dirscript /somedir kylie 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 somehow less obvious as well. You can imagine that it might be very easy for a user to mix up the right order of the arguments and type dirscript kylie sales /somedir instead. So it becomes important to provide good information on how to run this script.
Counting the Number of Script Arguments
On some occasions, you’ll want to check the number of arguments that are provided with a script. This is useful if you expect a certain number of arguments, for instance, and want to make sure that the required number of arguments is present before running the script.
To count the number of arguments provided with a script, you can use $#. Basically, $# is a counter that does no more than show you the exact number of arguments you’ve used when running the script. Used all by itself, that doesn’t really make sense. Combined with an if statement (about which you’ll read more in the section “Using if ... then ... else” later in this chapter) it does make sense. For example, you could use it to show a help message if the user hasn’t provided the correct number of arguments. Listing 14-11 shows the contents of the script countargs, in which $# is used. Directly following the code of the script, you can see a sample running of it.
Referring to all Script Arguments
So far, you’ve seen that a script can work with a fixed number of arguments. The example in Listing 14-10 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 $@ or $* in your script. Both refer to all arguments that were specified when starting the script, although there is a difference. To explain the difference, I need to show you how a for loop treats $@ or $*.
A for loop can be used to test all elements in a string of characters. Now what I want to show you at this point is that the difference between $@ and $* is exactly in the number of elements that each has. But let’s have a look at their default output first. Listing 14-12 shows version 1 of the showargs script.
Now let’s have a look at what happens if you launch this script with the arguments a b c d. You can see the result in Listing 14-13.
So far, there seem to be no differences between $@ and $*, yet there is a big difference: the collection of arguments in $* is treated as one text string, whereas the collection of arguments in $@ is seen as separate strings. In the section “Using for” later in this chapter, you will see some proof for this.
At this moment, you know how to handle a script that has an infinite number of arguments. You can tell the script that it should interpret them one by one. The next subsection shows you how to count the number of arguments.
Another elegant way to get input 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. The sample script askinput in Listing 14-14 shows a simple example script that first asks for the input and then shows the input that was provided by echoing the value of the variable. Directly following the sample code, you can also see what happens when you run the script.
As you can see, the script starts with an echo line that explains what it expects the user to do. Next, with 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. Formatting like this ensures 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. This is to prevent the shell from interpreting the special character before echo does. Again, if you want to make sure the shell does not interpret special characters like this, put the string between double quotes.
You may get confused here, because two different mechanisms are at work. First is the mechanism of escaping characters so that 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 tell the shell not to interpret anything that is between the double quotes, hence it shows .
The second mechanism is the special formatting character , which tells the shell to display a tab. To make sure that this or any other special formatting character is not interpreted by the shell when it first parses the script (which here would result in the shell just displaying a t), you have to put it between double quotes. In Listing 14-15, you can see the differences between all the possible commands.
When using echo -e, you can use the following special characters: