Chapter 19. An Introduction to Bash Shell Scripting

Image

The following topics are covered in this chapter:

Shell scripting is a science all by itself. You do not learn about all the nuts and bolts related to this science in this chapter. Instead, you learn how to apply basic shell scripting elements, which allows you to write a simple shell script and analyze what is happening in a shell script.

“Do I Know This Already?” Quiz

The “Do I Know This Already?” quiz allows you to assess whether you should read this entire chapter thoroughly or jump to the “Exam Preparation Tasks” section. If you are in doubt about your answers to these questions or your own assessment of your knowledge of the topics, read the entire chapter. Table 19-1 lists the major headings in this chapter and their corresponding “Do I Know This Already?” quiz questions. You can find the answers in Appendix A, “Answers to the ‘Do I Know This Already?’ Quizzes and ‘Review Questions.’

Table 19-1 “Do I Know This Already?” Section-to-Question Mapping

Foundation Topics Section

Questions

Understanding Shell Scripting Core Elements

1–2

Using Variables and Input

3–5

Using Conditional Loops

6–10

1. Which line should every Bash shell script start with?

a. /bin/bash

b. #!/bin/bash

c. !#/bin/bash

d. !/bin/bash

2. What is the purpose of the exit 0 command that can be used at the end of a script?

a. It informs the parent shell that the script could be executed without any problems.

b. It makes sure the script can be stopped properly.

c. It is required only if a for loop has been used to close the for loop structure.

d. It is used to terminate a conditional structure in the script.

3. How do you stop a script to allow a user to provide input?

a. pause

b. break

c. read

d. stop

4. Which line stores the value of the first argument that was provided when starting a script in the variable NAME?

a. NAME = $1

b. $1 = NAME

c. NAME = $@

d. NAME=$1

5. What is the best way to distinguish between different arguments that have been passed into a shell script?

a. $?

b. $#

c. $*

d. $@

6. What is used to close an if loop?

a. end

b. exit

c. stop

d. fi

7. What is missing in the following script at the position of the dots?

if [ -f $1 ]
then
     echo "$1 is a file"
..... [ -d $1 ]
then
       echo "$1 is a directory"
else
      echo "I do not know what $1 is"
fi

a. else

b. if

c. elif

d. or

8. What is missing in the following script at the position of the dots?

for (( counter=100; counter>1; counter-- )); .......
          echo $counter
done
exit 0

a. in

b. do

c. run

d. start

9. Which command is used to send a message with the subject “error” to the user root if something didn’t work out in a script?

a. mail error root

b. mail -s error root

c. mail -s error root .

d. mail -s error root < .

10. In a case statement, it is a good idea to include a line that applies to all other situations. Which of the following would do that?

a. *)

b. *

c. else

d. or

Foundation Topics

Understanding Shell Scripting Core Elements

Basically, a shell script is a list of commands that is sequentially executed, with some optional scripting logic in it that allows code to be executed under specific conditions only. To understand complex shell scripts, it is a good idea to start with an example of a very basic script, shown in Example 19-1.

Example 19-1 Basic Script Example

#!/bin/bash
#
# This is a script that greets the world
# Usage: ./hello

clear
echo hello world

exit 0

This basic script contains a few elements that should be used in all scripts. To start, there is the shebang. This is the line #!/bin/bash. When a script is started from a parent shell environment, it opens a subshell. In this subshell, different commands are executed. These commands can be interpreted in any way, and to make it clear how they should be interpreted, the shebang is used. In this case, the shebang makes clear that the script is a Bash shell script. Other shells can be specified as well. For instance, if your script contains Perl code, the shebang should be #!/usr/bin/perl.

It is good practice to start a script with a shebang; if it is omitted, the script code will be executed by the shell that is used in the parent shell as well. Because your scripts may also be executed by, for instance, users of ksh, using a shebang to call /bin/bash as a subshell is important to avoid confusion.

Right after the shebang, there is a part that explains what the script is about. It is a good idea in every script to include a few comment lines. In a short script, it is often obvious what the script is doing. If the script is becoming longer, and as more people get involved in writing and maintaining the script, it will often become less clear what the writer of the script intended to do. To avoid that, make sure that you include comment lines, starting with a #. Include them not only in the beginning of the script but also at the start of every subsection of the script. Comments will surely be helpful if you read your script a few months later and don’t remember exactly what you were trying to do while creating it. You can also use comments within lines. No matter in which position the # is used, everything from the # until the end of the line is comment text.

Next is the body of the script. In Example 19-1, the body is just a simple script containing a few commands that are sequentially executed. The body may grow as the script develops.

At the end of the script I have included the statement exit 0. An exit statement tells the parent shell whether the script was successful. A 0 means that it was successful, and anything else means that the script has encountered a problem. The exit status of the last command in the script is the exit status of the script itself, unless the exit command is used at the end of the script. But it is good to know that you can work with exit to inform the parent shell how it all went. To request the exit status of the last command, from the parent shell, use the command echo $?. This can be useful to determine whether and why something didn’t work out.

After writing a script, make sure that it can be executed. The most common way to do this is by applying the execute permission to it. So, if the name of the script is hello, use chmod +x hello to make it executable. The script can also be executed as an argument of the bash command, for instance. Use bash hello to run the hello script. If started as an argument of the bash command, the script does not need to be executable.

You can basically store the script anywhere you like, but if you are going to store it in a location that is not included in the $PATH, you need to execute it with a ./ in front of the script name. So, just typing hello is not enough to run your script; type ./hello to run it. Note that this is also required if you want to run the script from the current directory, because on Linux the current directory is not included in the $PATH variable. Or put it in a standard directory that is included in the $PATH variable, like /usr/local/bin. In Exercise 19-1 you apply these skills and write a simple shell script.

Exercise 19-1 Writing a Simple Shell Script

  1. Use vim to create a script with the name hello in your home directory.

  2. Give this script the contents that you see in Example 19-1 and close it.

  3. Use ./hello to try to execute it. You get a “permission denied” error message.

  4. Type chmod +x hello and try to execute it again. You see that it now works.

Using Variables and Input

Linux Bash scripts are much more than just a list of commands that is sequentially executed. One of the nice things about scripts is that they can work with variables and input to make the script flexible. In this section, you learn how to work with variables and input.

Using Positional Parameters

When starting a script, arguments can be used. An argument is anything that you put behind the script command while starting it. Arguments can be used to make a script more flexible. Take, for instance, the command useradd lisa. In this example, the command is useradd, and the argument lisa is specifying what needs to be done. In this case, a user with the name lisa has to be created. In this example, lisa is the argument to the command useradd. In a script, the first argument is referred to as $1, the second argument is referred to as $2, and so on. The example script in Example 19-2 shows how an argument can be used. Go ahead and try it using any arguments you want to use.

Example 19-2 Example Script That Is Using Arguments

#!/bin/bash
# run this script with a few arguments
echo The first argument is $1
echo The second argument is $2
echo The third argument is $3

If you tried to run the sample code from Example 19-2, you might have noticed that its contents are not perfect. If you use three arguments while using the script, it will work perfectly. If you only use two arguments, the third echo prints with no value for $3. If you use four arguments, the fourth value (which would be stored in $4) is never used. So, if you want to use arguments, you are better off using a more flexible approach. Example 19-3 shows an example of a script that is using a more flexible approach.

Example 19-3 Using Arguments in a Flexible Way

#!/bin/bash
# run this script with a few arguments
echo you have entered $# arguments
for i in "$@" do
            echo $i
done
exit 0

In Example 19-3, two new items that relate to the arguments are introduced:

  • $# is a counter that shows how many arguments were used when starting the script.

  • $@ refers to all arguments that were used when starting the script.

To evaluate the arguments that were used when starting this script, a conditional loop with for can be used. In conditional loops with for, commands are executed as long as the condition is true. In this script, the condition is for i in "$@", which means “for each argument.” Each time the script goes through the loop, a value from the $@ variable is assigned to the $i variable. So, as long as there are arguments, the body of the script is executed. The body of a for loop always starts with do and is closed with done, and between these two, the commands are listed that need to be executed. So, the example script in Example 19-3 will use echo to show the value of each argument and stop when no more arguments are available. In Exercise 19-2, you can try this for yourself by writing a script that works with positional parameters.

Exercise 19-2 Working with Positional Parameters

  1. Open an editor, create a script named ex192a, and copy the contents from Example 19-2 into this script.

  2. Save the script and make it executable.

  3. Run the command ./ex192a a b c. You see that three lines are echoed.

  4. Run the command ./ex192a a b c d e f. You see that still three lines are echoed.

  5. Open an editor to create the script ex192 and copy the contents from Example 19-3 into this script.

  6. Save the script and make it executable.

  7. Run the command ./ex192 a b c. You see that three lines are echoed.

  8. Run the command ./ex192 without arguments. You see that it does not echo anything.

Working with Variables

Key topic

A variable is a label that is used to refer to a specific location in memory that contains a specific value. Variables can be defined statically by using NAME=value or in a dynamic way. There are two solutions to define a variable dynamically:

Key topic
  • Use read in the script to ask the user who runs the script for input.

  • Use command substitution to use the result of a command and assign that to a variable. For example, the date +%d-%m-%y command shows the current date in day-month-year format. To assign that to a variable in a script, you could use TODAY=$(date +%d-%m-%y). In command substitution, you just have to enclose in parentheses the command whose result you want to use, with a dollar sign preceding the opening parentheses. As an alternative to this notation, backquotes may be used. So the command TODAY=`date +%d-%m-%y` would do exactly the same.

In the previous section about positional parameters, you learned how to provide arguments when starting a script. In some cases, it can be more efficient to ask for information when you find out that something essential is missing. The script in Example 19-4 shows how to do this using read.

Example 19-4 Example of a Script That Uses the read Command

#!/bin/bash
if [ -z $1 ]; then
          echo enter a name
          read NAME
else
          NAME=$1
fi
echo you have entered the text $NAME
exit 0

In Example 19-4, an if ... then ... else ... fi statement is used to check whether the argument $1 exists. This is done by using the test command, which can be written in either of two ways: test or [ ... ]. In Example 19-4, the line if [ -z $1 ] executes to see if the test -z $1 is true. The -z test checks whether $1 is nonexistent. Stated otherwise, the line if [ -z $1 ] checks whether $1 is empty; if so, it means that no argument was provided when starting this script. If this is the case, the commands after the then statement are executed. Notice that when writing the test command with the square brackets, it is essential to include one space after the opening bracket and one space before the closing bracket; without these spaces the command will not work.

Notice that the then statement immediately follows the test command. This is possible because a semicolon is used (;). A semicolon is a command separator and can replace a new line in a script. In the then statement, two commands are executed: an echo command that displays a message onscreen, and a read command. The read command stops the script so that user input can be processed and stored in the variable NAME. So, the line read NAME puts all user input in the variable NAME, which will be used later in the script.

In the example script in Example 19-4, the next part is introduced by the else statement. The commands after the else statement are executed in all other cases, which in this case means “if an argument was provided.” If that is the case, the variable NAME is defined and the current value of $1 is assigned to it.

Notice how the variable is defined: directly after the name of the variable there is an = sign, which is followed by $1. Notice that you should never use spaces when defining variables.

Then, the if loop is closed with a fi statement. Once the if loop has been completed, you know for sure that the variable NAME is defined and has a value. The last line of the script reads the value of the variable NAME and displays this value to STDOUT via the echo command. Notice that to request the current value of a variable, the variable name is referred to, preceded by a $ sign.

In Exercise 19-3, you can practice working with input.

Exercise 19-3 Working with Input

  1. Open an editor and create a script with the name ex193. Enter the contents of Example 19-4 in this script.

  2. Write the script to disk and use chmod +x ex193 to make it executable.

  3. Run the script using ./ex193 and no further arguments. You see that it prompts for input.

  4. Run the script using hello as its argument. It will echo “you have entered the text hello” to the STDOUT.

Using Conditional Loops

As you have already seen, you can use conditional loops in a script. These conditional loops are only executed if a certain condition is true. In Bash the following conditional loops are often used:

Key topic
  • if ... then ... else: Used to execute codes if a specific condition is true

  • for: Used to execute commands for a range of values

  • while: Used to execute code as long as a specific condition is true

  • until: Used to execute code until a specific condition is true

  • case: Used to evaluate specific values, where beforehand a limited number of values is expected

Working with if ... then ... else

The if ... then ... else construction is common to evaluate specific conditions. You have already seen an example with it in Example 19-4. This conditional loop is often used together with the test command, which you saw in action earlier to check whether a file exists. This command enables you to do many other things as well, such as compare files, compare integers, and much more.

Tip

Take a look at the man page of the test command.

The basic construction with if is if ... then ... fi. This evaluates one single condition, as in the following example:

if [ -z $1 ]
then
       echo no value provided
fi

In Example 19-4 you saw how two conditions can be evaluated by including else in the statement. Example 19-5 shows how multiple conditions can be evaluated by contracting else with if to become elif. This is useful if many different values need to be checked. Note in Example 19-5 that multiple test commands are used as well.

Example 19-5 Example with if ... then ... else

#!/bin/bash
# run this script with one argument
# the goal is to find out if the argument is a file or a directory
if [ -f $1 ]
then
       echo "$1 is a file"
elif [ -d $1 ]
then
       echo "$1 is a directory"
else
       echo "I do not know what $1 is"
fi
exit 0

Also note in Example 19-5 the use of the backslash (). This character informs the shell that it should not interpret the following character, which is known as escaping the character.

Using || and &&

Instead of writing full if ... then statements, you can use the logical operators || and &&. || is a logical OR and will execute the second part of the statement only if the first part is not true; && is the logical AND and will execute the second part of the statement only if the first part is true. Consider these two one-liners:

[ -z $1 ] && echo no argument provided
ping -c 1 10.0.0.20 2>/dev/null || echo node is not available

In the first example, a test is performed (using the alternative test command syntax) to see whether $1 is empty. If that test is true (which basically means that the test command exits with the exit code 0), the second command is executed.

In the second example, a ping command is used to check the availability of a host. The logical OR is used in this example to echo the text “node is not available” in case the ping command was not successful. You’ll often find that instead of fully written if ... then statements, the && and || constructions are used. In Exercise 19-4 you can practice some if ... then ... else skills, using either if ... then ... else or && and ||.

Exercise 19-4 Using if ... then ... else

In this exercise, you work on a script that checks the availability of the Apache web server.

  1. Start an editor and create a script with the name filechk.

  2. Copy the contents from Example 19-5 to this script.

  3. Run a couple of tests with it, such as ./filechk /etc/hosts, ./filechck /usr, and ./filechk non-existing-file.

Applying for

The for conditional provides an excellent solution for processing ranges of data. In Example 19-6, you can see the first example with for, where a range is defined and processed as long as there are unprocessed values in that range.

Example 19-6 Example with for

#!/bin/bash
#
for (( COUNTER=100; COUNTER>1; COUNTER-- )); do
          echo $COUNTER
done
exit 0

A for conditional statement always starts with for, which is followed by the condition that needs to be checked. Then comes do, which is followed by the commands that need to be executed if the condition is true, and the conditional statement is closed with done.

In the example in Example 19-6, you can see that the condition is a range of numbers assigned to the variable COUNTER. The variable first is initialized with a value of 100, and as long as the value is higher than 1, in each iteration 1 is subtracted. As long as the condition is true, the value of the $COUNTER variable is displayed, using the echo commands.

In Example 19-7, you can see one of my favorite one-liners with for. The range is defined this time as a series of numbers, starting with 100 and moving up to 104.

Example 19-7 Example One-Liner with for

for i in {100..104}; do ping -c 1 192.168.4.$i >/dev/null && echo
  192.168.4.$i is up; done

Notice how the range is defined: You specify the first number, followed by two dots and closed with the last number in the range. With for i in, each of these numbers is assigned to the variable i. For each of these numbers, a ping command is executed, where the option -c 1 makes sure that one ping request only is sent.

In this ping command, it is not the result that counts, which is why the result is redirected to the /dev/null device. Based on the exit status of the ping command, the part behind the && is executed. So, if the host could be reached, a line is echoed indicating that it is up.

Understanding while and until

Whereas the for statement that you have just read about is useful to work through ranges of items, the while statement is useful if you want to monitor something like the availability of a process. The counterpart of while is until, which keeps the iteration open until a specific condition is true. In Example 19-8 you can see how while is used to monitor process activity.

Example 19-8 Monitoring Processes with while

#!/bin/bash
#
# usage: monitor <processname>
while ps aux | grep $1 | grep -v grep > /dev/tty11
do
      sleep 5
done

clear
echo your process has stopped
logger $1 is no longer present
mail -s "process $1 has stopped" root <.

The script in Example 19-8 consists of two parts. First, there is the while loop. Second, there is everything that needs to be executed when the while loop no longer evaluates to true. The core of the while loop is the ps command, which is grepped for the occurrence of $1. Notice the use of grep -v grep, which excludes lines containing the grep command from the result. Keep in mind that the ps command will include all running commands, including the grep command that the output of the ps command is piped to. This can result in a false positive match. The results of the ps aux command are redirected to /dev/tty11. That makes it possible to read the results later from tty11 if that is needed, but they do not show by default.

After the while statements, follow the commands that need to be executed if the statement evaluates to true. In this case, the command is sleep 5, which will basically pause the script for 5 seconds. As long as the while command evaluates to true, it keeps on running. If it does no longer (which in this case means that the process is no longer available), it stops and the commands that follow the while loop can be executed.

In the line mail -s “process $1 has stopped” root < ., a message is sent to the user root, using the internal mail handler that runs on RHEL 8 by default. The mail command takes as its first argument the subject, specified using the -s option. Notice the < . at the end of the command.

Normally, when using the mail command in an interactive mode, it will open an editor in which the message body can be written. This editor is closed by providing a line that has only a dot. In this command, the dot is provided through redirection of the STDIN. This allows the message to be processed without any further requirement for user activity.

The counterpart of while is until, which opens an iteration that lasts until the condition is true. In Example 19-9, until is used to filter the output of the users command for the occurrence of $1, which would be a username. Until this command is true, the iteration continues. When the username has been found in the output of users, the iteration closes and the commands after the until loop are executed.

Example 19-9 Monitoring User Login with until

#!/bin/bash
#
until users | grep $1 > /dev/null
do
    echo $1 is not logged in yet
    sleep 5
done
echo $1 has just logged in
mail -s "$1 has just logged in" root <.

Understanding case

The last of the important iteration loops is case. The case statement is used to evaluate a number of expected values. The case statement in particular is important in Linux startup scripts that on previous versions of RHEL were used to start services. In a case statement, you define every specific argument that you expect, which is followed by the command that needs to be executed if that argument was used.

In Example 19-10, you can see the blueprint of the case statement that was used in the service scripts in earlier versions of RHEL to start almost any service. This statement works on $1, which is the name of a startup script. Following the name of the script, the user can type start, stop, restart, and so on.

Example 19-10 Evaluating Specific Cases with case

case "$1" in
   start)
           start;;
   stop)
            rm -f $lockfile
            stop;;
   restart)
           restart;;
   reload)
           reload;;
    status)
            status
           ;;
   *)
           echo "Usage: $0 (start|stop|restart|reload|status)"
           ;;
esac

The case statement has a few particularities. To start, the generic syntax is case item-to-evaluate in. Then follows a list of all possible values that need to be evaluated. Each item is closed with a ). Then follows a list of commands that need to be executed if the specific argument was used. The list of commands is closed with a double semicolon. This ;; can be used directly after the last command, and it can be used on a separate line. Also notice that the *) refers to all other options not previously specified. It is a “catchall” statement. The case iteration loop is closed by an esac statement.

Notice that the evaluations in case are performed in order. When the first match is made, the case statement will not evaluate anything else. Within the evaluation, wildcard-like patterns can be used. This shows in the *) evaluation, which matches everything. But you could as well use evaluations like start|Start|START) to match the use of a different case.

Bash Shell Script Debugging

When a script does not do what you expect it to do, debugging the script is useful. If a script does not do what you expect it to do, try starting it as an argument to the bash -x command. This will show you line by line what the script is trying to do, and also will show you specific errors if it does not work. Example 19-11 shows an example of using bash -x where it becomes immediately clear that the grep command does not know what it is expected to do, which is because it misses an argument to work on.

Example 19-11 Using bash -x to Debug Scripts

[root@server1 ~]# bash -x 319.sh
+ grep
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
+ users
+ echo is not logged in yet
is not logged in yet
+ sleep 5

Summary

In this chapter you learned how to write shell scripts. You’ve worked through a few examples and are now familiar with some of the basic elements that are required to create a successful script.

Exam Preparation Tasks

As mentioned in the section “How to Use This Book” in the Introduction, you have several choices for exam preparation: the end-of-chapter labs; the memory tables in Appendix B; Chapter 26, “Final Preparation”; and the practice exams.

Review All Key Topics

Review the most important topics in the chapter, noted with the Key Topic icon in the outer margin of the page. Table 19-2 lists a reference of these key topics and the page number on which each is found.

Key topic

Table 19-2 Key Topics for Chapter 19

Key Topic Element

Description

Page

Paragraph

Definition of variable

430

List

Dynamically defining variables

430

List

Conditional loops overview

432

Define Key Terms

Define the following key terms from this chapter and check your answers in the glossary:

shebang

parent shell

subshell

variable

iteration

conditional loop

OR

AND

Review Questions

The questions that follow are meant to help you test your knowledge of concepts and terminology and the breadth of your knowledge. You can find the answers to these questions in Appendix A.

1. What is the effect if a script does not start with a shebang?

2. How can you check if a variable VAR has no value?

3. What would you use in a script to count the number of arguments that have been used?

4. What would you use to refer to all arguments that have been used when starting the script?

5. How do you process user input in a script?

6. What is the simplest way to test whether a file exists and execute the command echo “file does not exist” if it does not?

7. Which test would you perform to find out if an item is a file or a directory?

8. Which construction would you use to evaluate a range of items?

9. How do you close an elif statement in a script?

10. In a case statement, you evaluate a range of items. For each of these items you execute one or more commands. What do you need to use after the last command to close the specific item?

End-of-Chapter Lab

In this end-of-chapter lab, you apply your scripting skills to write two simple scripts.

Lab 19.1

1. Write a script that works with arguments. If the argument one is used, the script should create a file /tmp/one. If the argument two is used, the script should send a message containing the subject “two” to the root user.

2. Write a countdown script. The script should use one argument (and not more than one). This argument specifies the number of minutes to count down. It should start with that number of minutes and count down second by second, writing the text “there are nn seconds remaining” at every iteration. Use sleep to define the seconds. When there is no more time left, the script should echo “time is over” and quit.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.120.204