Shell Scripting - CompTIA Linux+ / LPIC-1 Cert Guide (Exams LX0-103 & LX0-104/101-400 & 102-400) (2016)

CompTIA Linux+ / LPIC-1 Cert Guide (Exams LX0-103 & LX0-104/101-400 & 102-400) (2016)

Chapter 12. Shell Scripting

This chapter covers the following topics:

Image Basics of Scripting

Image Shell Script Commands

This chapter covers the following exam objectives:

Image Customize or write shell scripts: 105.2

Shell scripting is about writing your own tools to automate some of your routine work. Is there a procedure you have that involves a handful of commands? A shell script can turn it into one command. The time you invest in learning to script will pay for itself many times over with the time you save.

You don’t have to be a programmer to be a good scripter. You already know how to work with the Linux command line and all scripting starts from there.

“Do I Know This Already?” Quiz

The “Do I Know This Already?” quiz enables you to assess whether you should read this entire chapter or simply jump to the “Exam Preparation Tasks” section for review. If you are in doubt, read the entire chapter. Table 12-1 outlines the major headings in this chapter and the 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.”

Image

Table 12-1 “Do I Know This Already?” Foundation Topics Section-to-Question Mapping

1. A Bash comment starts with which character?

a.

b. !

c. #

d. --

2. You have written a Perl script to send emails to users who leave processes that abuse the CPU. Rather than perl cpu_report.pl, you want to run it as ./cpu_report.pl. Which two steps are needed?

a. Put #!/usr/bin/perl on the first line of the script.

b. chmod +x cpu_report.pl.

c. chown root cpu_report.pl; chmod +s cpu_report.pl.

d. Put #!perl on the first line of the script.

e. Put the script in /usr/bin along with perl.

3. Reviewing a shell script you found, you see this:

if [[ -x /etc/zoid ]]; then
. /etc/zoid
elif [[ -x $HOME/.zoid ]]; then
. $HOME/.zoid
fi

Which of the following is true?

a. /usr/bin/elif needs to be present for this to work.

b. The script will run /etc/zoid and $HOME/.zoid if they exist.

c. If /etc/zoid is marked as executable, it will be executed.

d. $HOME/.zoid takes priority over /etc/zoid.

4. Consider the following transcript:

$ ./report.pl
$ echo $?
1

What can be said about what just happened?

a. The command completed successfully.

b. One argument was passed to report.pl through the environment.

c. The script ran for 1 second.

d. An error happened during the script.

5. During a script’s execution, what is stored in $1?

a. The first argument to the script

b. The shell that called the script

c. The name of the script

d. The process ID of the script

6. You are using the scripting statement case in a script and keep getting a message such as the following:

script1: line 10: syntax error: unexpected end of file

What is the likely cause of the error?

a. You didn’t have a default condition set.

b. You forgot to close the case with esac.

c. You were using the old [ ] bash style instead of [[ ]].

d. You were comparing an integer when you should have been comparing a string.

7. Given a directory full of files, how could you rename everything to have a .bak extension?

a. mv * *.bak

b. for i in ls; do mv $i $i.bak; done

c. for i in ls; do mv i i.bak; done

d. for i in *; do mv $i $i.bak; done

Foundation Topics

Basics of Scripting

A shell script is just a text file containing some commands. The following is a typical script used to deploy new software:

#!/bin/bash
# Deploy the application. Run as deploy foo-1.2.war
cp $1 /usr/local/tomcat/webapps/application.war
service tomcat restart
echo "Deployed $1" | mail group@example.com

Line by line, this script works as follows:

Image Line 1—Known as the shebang line after the hash or sharp (#) bang (!) that starts it, this tells the kernel that the script should be interpreted by /bin/bash instead of run as a binary.

Image Line 2—Comments begin with a # and are to help humans understand the program. Computers just ignore the # and everything after it on that line.

Image Line 3—$1 is the first argument passed to the script on the command line. This line copies that file to the destination directory.

Image Line 4—Runs a script to restart the service.

Image Line 5—Sends an email.

Other than the $1 and shebang line, this script is identical to what you would do at the command line. With only three commands you might wonder what the value of putting it into a shell script would be:

Image Even if it takes a minute to run the manual way, it’s a distraction because it consumes your attention for the whole procedure.

Image You may make mistakes.

Image It may be three commands today, but it’s sure to grow over time.

Image You can’t take vacation without writing down the procedure and training your coworkers.

All these problems can be solved with scripting. Scripts do the same thing every time. You can run it in a window and get back to what you were doing. You can add commands to the script over time. Instead of complicated documentation and training, you just need to teach people how to run your script.

Running a Script

There are two ways to run a script. The first is the same as if it were a binary application:

$ ./test.sh
Hello, world

This format requires the shebang line as the first line in order to work and that the script be marked as executable. The shell tells the kernel to run the script. The kernel sees the special line and knows it’s supposed to get that interpreter to run the script.

Image

Try it out yourself and you might find that it works without the shebang line. That’s the shell being smart. You should always have the #!/bin/bash there because you don’t always know how the script will be executed or whether the user might be using a different shell.

The second way is to pass the script as an argument to a new shell:

$ bash test.sh
Hello, world

This has fewer requirements than the other method. The script doesn’t have to be marked as executable. The shebang line is treated as a comment in this case. There’s no question about which interpreter should run the script—you said to use bash on the command line.

Good Design

Before you start writing shell scripts you should think about what makes a good shell script. At a high level you’re not writing the script for yourself, you’re writing it for your coworker who may have to use it at 2:00 o’clock in the morning. A good script:

Image Does one thing, and does it well.

Image Prints descriptive error and success messages.

Image Is written in a simple manner and uses comments to make steps obvious to someone looking at the source.

Image Has a name that communicates its intent. deploy_website.sh is good. The name of your favorite football team is not.

Image Has a file extension that indicates how it’s to be run, such as .sh.

Follow these guidelines and your scripts will be maintainable long after you wrote them, and you won’t have to explain things over and over to coworkers.

Managing Your Scripts

You should try to keep your scripts in a predictable place. There’s nothing like spending time automating something and then forgetting where you put your scripts!

The location of the scripts depends on your environment. If you have coworkers to share with, or many computers, you will want your scripts in a more public location. /usr/local/ may be mounted on all your computers, so placing your scripts in /usr/local/bin makes it as widely available as possible.

Another option is to keep your code in $HOME/bin and find a way to share with other people. A version control system (VCS) such as git or svn is a good choice. Not only does a VCS make it easy to share with people, but it tracks the history of your changes.

Either way the script should be marked as executable and have the shebang header. This allows people to run it directly if they want. The executable flag also signals to other people that the script is intended to be run as opposed to being a text file.

You learned earlier that setting the setuid bit on an executable file allows it to run with the owner’s permissions instead of the person who ran it. That is, a file that was owned by root and has setuid enabled would run as root even if a nonprivileged user ran it.

The Linux kernel does not honor the setuid bit on script files. If you need to give elevated privileges within a script you must use something like sudo.

Shell Script Commands

Any command from the command line is fair game within a script. The shell itself provides some built-in commands that make shell scripting more powerful.

Use the Output of Another Command

You will frequently need to run a command and capture the output in a variable for later use. For example, you can find the process IDs of your web server:

$ ps -ef | grep nginx
root 4846 1 0 Mar11 ? 00:00:00 nginx: master process
/usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx 6732 4846 0 Mar11 ? 00:00:12 nginx: worker process
nginx 19617 2655 0 18:54 ? 00:00:01 php-fpm: pool www
nginx 19807 2655 0 19:01 ? 00:00:00 php-fpm: pool www
nginx 19823 2655 0 19:03 ? 00:00:00 php-fpm: pool www
sean 20007 19931 0 19:07 pts/0 00:00:00 grep nginx

And from there you can pick out just the master process by fine-tuning your grep statement:

$ ps -ef | grep "nginx: master process"
root 4846 1 0 Mar11 ? 00:00:00 nginx: master process
/usr/sbin/nginx -c /etc/nginx/nginx.conf
sean 20038 19931 0 19:09 pts/0 00:00:00 grep nginx: master
process

You can weed out the grep line, which is the grep you ran to find the process, by using a regular expression that matches the nginx process but not the grep command line.

$ ps -ef | grep "[n]ginx: master process"
root 4846 1 0 Mar11 ? 00:00:00 nginx: master process
/usr/sbin/nginx -c /etc/nginx/nginx.conf

And finally, extract column 2, which is the process ID.

$ ps -ef | grep "[n]ginx: master process" | awk '{ print $2 }'
4846

Image

Enclosing the last command in backticks within your script gets you the output in a variable, which is known as command substitution:

PID=`ps -ef | grep "[n]ginx: master process" | awk '{ print $2 }'`
echo nginx is running at $PID

Another way you might see this written is in the $() style:

PID=$(ps -ef | grep "[n]ginx: master process" | awk '{ print $2 }')
echo nginx is running at $PID

Both methods do the same thing. Parentheses are easier to match up when debugging problems, which makes the second method better to use.

You can then use the PID variable to kill the process or restart it:

echo Telling nginx to reopen logs using pid $PID
kill -USR1 $PID

Do Math

When your teachers told you that you will need math to do work, they were right. There are many cases where being able to do some easy math in a shell script helps.

If you want to know the number of processes on a system, you can run ps -ef | wc -l. ps -ef gives you the process list; wc -l returns the number of lines. That gives you one more than the number you are looking for because ps includes a header line.

Image

The shell can do math by enclosing the expression inside $(()).

PROCS=$(ps -ef | wc -l)
PROCS=$(($PROCS-1))

Or on one line:

PROCS=$(( $(ps -ef | wc -l) - 1 ))

That is starting to get unreadable, so backticks might be better:

PROCS=$(( `ps -ef | wc -l` - 1 ))

Earlier you were told to assign variables using the name of the variable, such as FOO=bar, and to use their values with the dollar sign prefix such as $FOO. The $(()) syntax works with either.

Bash only works with integers, so if you need decimal places you need a tool like bc.

RADIUS=3
echo "3.14 * $RADIUS ^ 2" | bc

bc accepts an arithmetic expression on the input and returns the calculation on the output. Therefore you must echo a string into the tool. The quotes are necessary to prevent the shell from interpreting elements as a file glob, as the asterisk was intended for bc and not to match a file.

Conditions

Scripts don’t have to run start to finish and always do the same thing. Scripts can use conditionals to test for certain cases and do different things depending on the output of the test.

if ps -ef | grep -q [n]agios; then
echo Nagios is running
fi

Image

The if statement executes some statements and if the condition is true, the code between the then and the fi is run. In the previous example the code is ps -ef | grep -q [n]agios, which looks for nagios in the process listing using the regular expression to exclude the grep command itself. The -q argument tells grep not to print anything (to be quiet) because all you care about is the return value, which is stored in the $? variable.

$ ps -ef | grep -q [n]agios
$ echo $?
0
$ ps -ef | grep -q [d]oesNotExist
$ echo $?
1

Image

$? holds the return code of the last command executed, which is the grep. The return code is 0 when the string is matched and 1 when it isn’t. Anything greater than 1 indicates some kind of error, so it’s generally better to test for either 0 or any positive integer.


Note

In most computer situations 1 is true and 0 is false. With Bash programming it’s the opposite.


If you want to test for the absence of a condition, you can negate the test statement with an exclamation point:

if ! ps -ef | grep -q [n]agios; then
echo Nagios is NOT running
fi

Often you want to do one thing if the condition holds true and another thing when it’s false, which is where else comes in.

if ps -ef | grep -q [n]agios; then
echo Nagios is running
else
echo Nagios is not running. Starting
service nagios start
fi

Here, if the grep condition is true the script just prints a statement to the screen. If it is false it prints a statement and starts the service.

Image

Perhaps you have three possibilities, in which case you need elif, which is short for “else if.”

if ps -ef | grep -q [h]ttpd; then
echo Apache is running
elif ps -ef | grep -q [n]ginx; then
echo Nginx is running
else
echo No web servers are running
fi

Testing Files

There is a range of common cases where writing a shell command to test for a condition would be awkward so the test command was introduced. With test you can perform a series of checks on files, strings, and numbers.

if test -f /etc/passwd; then
echo "password file exists"
fi

The test command reads a series of options up until the semicolon or end of line and returns a value depending on the results of the test. The -f test checks for the existence of a file; in the previous case it is /etc/passwd. If the file exists, the echo will be run.

Image

There are many other tests. Some accept one filename and others only test one file.

Image FILE1 -ef FILE2—FILE1 and FILE2 have the same device and inode numbers.

Image FILE1 -nt FILE2—FILE1 is newer (modification date) than FILE2.

Image FILE1 -ot FILE2—FILE1 is older than FILE2.

Image -d FILE—FILE exists and is a directory.

Image -e FILE—FILE exists.

Image -f FILE—FILE exists and is a regular file.

Image -h FILE—FILE exists and is a symbolic link.

Image -r FILE—FILE exists and the user can read it.

Image -s FILE—FILE exists and has a size greater than zero.

Image -w FILE—FILE exists and the user can write to it.

Image -x FILE—FILE exists and the user has the execute bit set.

There are even more options than that; the test man page has more details.

An Easier Test Syntax

The test command is used so much that Bash has the “square brackets” [ ] shortcut.

if [ conditions ]; then

Image

Anything between the square brackets is considered a test. The test to see whether /usr/bin/nginx is executable would then be

if [ -x /usr/bin/nginx ]; then
echo nginx is executable
fi

Newer versions of Bash can also use two square brackets:

if [[ -x /usr/bin/nginx ]]; then
echo nginx is executable
fi

The new style is more forgiving of errors, such as if you’re using a variable that’s unset. An unset variable has not been assigned a value and therefore contains nothing when you try to reference its contents. If you’re dealing entirely with Linux systems, it is the safer option to use.

The syntax of this style is important. There must be a space before and after the opening square bracket and a space before the closing square bracket. The space after the closing square bracket is optional because the semicolon or line ending is enough to tell the shell that the test has finished. The shell can’t differentiate between the square brackets and the test itself if the spaces are missing. Thus the following commands are invalid:

if[[ -x /usr/bin/nginx ]]; # no space before first brackets
if [[-x /usr/bin/nginx ]]; # no space after first brackets
if [[ -x /usr/bin/nginx]]; # no space before final brackets

Testing Strings

Words, also known as strings, can be easily tested, too.

echo -n "Say something: "
read STRING
if [[ -z $STRING ]]; then
echo "You didn't say anything"
else
echo Thanks for that
fi

The first line prints a prompt to the screen; the -n option eliminates the newline at the end. The read command stores the data gathered from stdin (typically data typed by the user) and places the input into the variable called STRING.

The test uses -z to check for a zero length string. If the user didn’t enter anything, the first condition is run. Anything else and the second condition is used.

The opposite of -z is -n, which tests for a nonzero length string.

Image

String equality is tested with a single equals sign:

if [[ `hostname` = 'bob.ertw.com' ]]; then
echo You are on your home machine
else
echo You must be somewhere else
fi

The conditional expression does not have to include a variable. In the preceding example the output of the hostname command (using command substitution) is compared to a string.

The opposite of = is !=.

Testing Integers

Image

Strings and integers are treated differently and as such, need different operators to test. There are six integer operators:

Image X -eq Y—Tests whether X is equal to Y.

Image X -ne Y—Tests whether X is not equal to Y.

Image X -gt Y—Tests whether X is greater than Y.

Image X -ge Y—Tests whether X is greater than or equal to Y.

Image X -lt Y—Tests whether X is less than Y.

Image X -le Y—Tests whether X is less than or equal to Y.

You may want to count processes, files, number of users, or elapsed time.

LASTRUN=$(cat lastrun)
NOW=$(date +%s)
ELAPSED=$((NOW - LASTRUN))

if [[ $ELAPSED -ge 60 ]]; then
echo A minute or more has passed since the last time you ran this
script
echo $NOW > lastrun
fi

In the preceding script the script looks for a file with the name of lastrun and reads the contents into memory. It also reads the current timestamp in seconds since epoch and then calculates the difference into a third variable.

The script can then check to see whether a minute or more has elapsed, take an action, and then reset the last run timestamp.

Combining Multiple Tests

All the previous examples addressed the need to run a single test. More complicated tests can be run that consider multiple options. This is the realm of Boolean logic:

Image A AND B—True if both A and B are true.

Image A OR B—True if either A or B is true.

Image

With test and [ condition ], AND and OR are handled with -a and -o, respectively. Inside [[ condition ]] blocks, use && and ||.

if [ -f /etc/passwd -a -x /usr/sbin/adduser ]
if [[ -f /etc/passwd && -x /usr/sbin/adduser ]]

Boolean logic assigns a higher precedence to AND than it does OR. It’s like BEDMAS in arithmetic: Brackets happen before exponents, then division and multiplication, then addition and subtraction. In Boolean logic the order is

1. Brackets

2. AND

3. OR

Therefore [[ A || B && C ]] is evaluated as [[ A || ( B && C ) ]]. This is a great example of how to make your code more clear for the people reading it. If you can write your tests so that there’s no way to be confused by the order of operations, then do it!

Case Statements

if/elif/else statements get bulky if you have more than three possibilities. For those situations you can use case, as shown in Example 12-1.

Example 12-1 Using case Instead of if/elif/else


case $1 in
start)
echo "Starting the process"
;;
stop)
echo "Stopping the process"
;;
*)
echo "I need to hear start or stop"
esac


A case statement starts with a description of what the value to be tested is in the form case $variable in. Then each condition is listed in a specific format:

1. First comes the string to be matched, which is terminated in a closing parenthesis. The * matches anything, which makes it a default.

2. Then comes zero or more statements that are run if this case is matched.

3. The list of statements is terminated by two semicolons (;;).

4. Finally the whole statement is closed with esac.

Processing stops after the first match, so if multiple conditions are possible only the first one is run.

The string to be matched can also be matched as if it were a file glob, which is helpful to accept a range of input, as shown in Example 12-2.

Example 12-2 A String Matched as if It Were a File Glob


DISKSPACE=$(df -P / | tail -1 | awk '{print $5}' | tr -d '%')
case $DISKSPACE in
100)
echo "The disk is full"
echo "Emergency! Disk is full!" | mail -s "Disk emergency" root@
example.com
;;
[1-7]*|[0-9])
echo "Lots of space left"
;;
[8-9]*)
echo "I'm at least 80% full"
;;
*)
echo "Hmmm. I expected some kind of percentage"
esac


In this example the DISKSPACE variable contains the percentage of disk space used, which was obtained by command substitution as follows:

Image Take the output of df -P /, which shows just the disk used for the root filesystem in a single line.

Image Pipe it through tail -1, which only prints the last line so that the header is removed.

Image Print the fifth column, which is the percentage of the disk that’s free, by piping through awk.

Image Remove the percent character with the translate (tr) command.

If the disk usage is 100, a message indicating a disk is full and the script even sends an email to root with an appropriate subject line using -s. The second test looks for something that begins with the numbers 1 through 7 and optionally anything after, or just a single digit. This corresponds to anything under 80% full. The third test is anything beginning with 8 or 9. The first match wins rule ensures that a disk that is 9% full is caught by the second rule and doesn’t pass through to the third.

Finally a default value is caught just in case bad input is passed.

Loops

Loops are the real workhorses of shell scripting. A loop lets you run the same procedure over and over.

Bash offers two types of loops. The for loop iterates over a fixed collection. The while loop keeps running until a given condition is met.

For Loops

The for loop’s basic syntax is

for variable in collection; do
# Do something on $variable
done

The collection can be fixed:

$ for name in Ross Sean Mary-Beth; do
> echo $name
> done
Ross
Sean
Mary-Beth

Here the collection is a list of three names, which start after the in and go up to the semicolon or the end of the line. Anything that you can do in a shell script can be done directly on the command line! Loops are powerful tools that can save work both inside shell scripts and in ad-hoc commands.

Image

Each time around the loop the variable called name holds the current value. For this example the loop executes three times.

Note that the declaration of the variable is just the name of the variable and using the variable requires the dollar sign prefix.

The collection can be a file glob:

for file in *.txt; do newname=`echo $file | sed 's/txt$/doc/'`; mv
$file $newname; done

This example, all on one line, renames every file ending in .txt to .doc. It does this by iterating over the *.txt wildcard and each time setting the variable file to the current file. It assigns a temporary variable, newname, to the output of a sed command that replaces the extension with the new one. Finally it moves the old name to the new name before moving on to the next file.

Sequences

Loops are just as happy iterating over the output of another command as they are a static list of names or a file glob. The seq command is particularly suited to this task. It counts from a starting point to an ending point for you:

$ seq 1 5
1
2
3
4
5

Or counts by twos when the arguments are start, skip, and end:

$ seq 1 2 5
1
3
5

Or pads the output with leading zeroes if the number of digits changes through the -w flag:

$ seq -w 8 10
08
09
10

With that in mind, iterating over something 10 times becomes straightforward:

for i in $(seq -w 1 10); do
curl -O http://example.com/downloads/file$i.html
done

Here the loop counter is more than just to count the number of iterations. The counter, i, forms part of a URL on a remote web server. Also note that the seq command included -w so that the numbers were all two digits, which is common in remote web files. Ultimately this script downloads 10 files with a predictable name from a remote website using curl.

While Loops

Some kinds of loops have to run for an unpredictable period of time. What if you had a script that couldn’t run at the same time as another program, so it had to wait until the other program finished?

while [[ -f /var/lock/script1 ]] ; do
echo waiting
sleep 10
done

Image

The command on the while line looks for the presence of a lock file and pauses for 10 seconds if it’s found. The file is checked each time through the loop and the loop ends only when the file is removed.

If you wanted to, you could also count the same way you did with for and seq:

value=0
last=33
while [ $value -lt $last ]
do
echo $value
value=$(($value + 1))
done

This counts from 0 to 32. It is 32 instead of 33 because the condition is -lt—less than—instead of –le, which is less than or equal to.

The opposite of while is until, which behaves as if the loop condition were written with an exclamation point in front of it. In other words the loop continues while the expression is false and ends the loop once the condition is true. You could use this if you’re waiting for another script to drop a file in a certain location:

until [ -f /var/tmp/report.done ]; do

# Waiting until the report is done

sleep 10

done

rm /var/tmp/report.done

This program waits until a file is present, such as from a report job that is run. It then cleans up the state file and continues on.

Reading from stdin in a Loop

While loops are great for reading input from the console. This script behaves the same as the cat command. It echoes anything passed to it as a filter to the output.

while read LINE; do
echo $LINE
done

This technique is less efficient than piping the output through a series of other tools, but sometimes you need to perform complicated logic on a line-by-line basis, and this is the easiest way to do that.

Image

read can also be used to accept input from the user:

echo -n "What is your name? "
read NAME
echo Hello, $NAME

Interacting with Other Programs

A shell script is expected to run other programs to do its job. If you use cp, awk, sed, grep, or any other Unix utility, you are running another program.

Programs you run might produce output that you want to ignore. You already saw that command substitution lets you capture the output, but if you don’t want to see the output you need to use redirection.

Image

The grep command can produce warnings if it reads a binary file or can’t read a file because of missing permissions. If you’re fine with these errors happening, you can eliminate them by redirecting the output to /dev/null and also redirecting the error stream to the regular output stream:

grep -r foo /etc > /dev/null 2>&1

You can still test the return status of this command by looking at $? or the usual if/while/test commands. The only difference is the output has been thrown away.

Returning an Error Code

You’ve seen how testing the return value of another program can let you branch and do different things based on the output. It’s possible to make your own scripts return a success or error condition to whoever is calling it.

Image

You can use the exit keyword anywhere within your program. By itself it returns 0, the success value. If you write exit 42, it returns 42. The caller sees this number in $?.

Be careful to return something that makes sense. The shell expects that successful execution returns an exit code of 0. If you are going to return anything other than the typical 0 for success and 1 for error, you should describe that in comments at the top of your code.

Accepting Arguments

Your script can be run with arguments:

./deploy foo.war production

Image

Each of the arguments is stored in $1, $2, $3, and so forth. $0 is the name of the script itself.

The special bash variable $# contains the number of arguments passed to the script. You can use this to provide some basic error checking of the input.

if [[ $# -lt 2 ]]; then
echo Usage: $0 deployable environment
exit 1 # return an error to the caller
fi

Perhaps you’re dealing with an unknown number of arguments. The shift keyword moves $2 to $1, $3 to $2, and so forth. Shifting at the end of the loop means that $1 is always your current argument, as shown in Example 12-3.

Example 12-3 Using the shift Keyword


while [[ $# -gt 0 ]]; do
echo Processing $1
shift
echo There are $# to go
done
$ ./test.sh one two three
Processing one
There are 2 to go
Processing two
There are 1 to go
Processing three
There are 0 to go


Using shift is helpful when you are processing an unknown list of options, such as filenames.

Transferring Control to Another Program

A bit more esoteric way of calling a program is to use the exec command. Normally when you call a program the shell creates a new process and runs the program you called. However, it’s possible to replace your currently running program with a new one.

If you want to know the dirty details of how a program is run, it’s actually a combination of both methods. A subprocess is started by forking the existing process into two: the parent (original) and child (new). The two processes are identical at this point. The child program then executes the intended program.

For example, when you type ls into your bash prompt, bash forks itself into two. The child is an identical copy of your shell. That child then executes the ls command, replacing itself with ls. The parent waits until the ls exits, and returns a shell prompt.

If you call exec on a program from within a script, or even from the shell, your session is replaced. When the file you call has finished running, control returns to whatever called the original script, not your script.

For example:

echo Hi
exec sleep 1
echo There

This simple program prints a line of text, execs a 1 second pause, and then prints another line. When you run it, however:

$ sh test.sh
Hi
$

Processing stopped after the sleep! The shell script was replaced, in memory, by a sleep. Control was returned to the shell that called test.sh, and not the line following the exec.

This technique is used when a shell script’s job is to launch another program and it doesn’t matter what happens after the program exits. The new program has access to all the variables and context even if they haven’t been exported.

Exam Preparation Tasks

As mentioned in the section “How to Use This Book” in the Introduction, you have a couple of choices for exam preparation: the exercises here, Chapter 21, “Final Preparation,” and the practice exams on the DVD.

Review All Key Topics

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

Image

Image

Table 12-2 Key Topics for Chapter 12

Define Key Terms

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

shebang

version control system

command substitution

conditionals

strings

Boolean logic

Review Questions

The answers to these review questions are in Appendix A.

1. On a Linux system installed with the default shell, you need to execute a shell script that contains variables, aliases, and functions by simply entering its name on the command line. What should be the first line of the script? (Choose all that apply.)

a. Nothing

b. #/bin/csh

c. #!/bin/bash

d. exec=/bin/bash

2. If ps -ef | grep nginx | awk ‘{print $2}’ returns a list of process ids for nginx, how would you kill them all in one line?

a. kill “ps -ef | grep nginx | awk ‘{print $2}’”

b. PIDS=”ps -ef | grep nginx | awk ‘{print $2}’”; kill PIDS

c. kill $(ps -ef | grep nginx | awk ‘{print $2}’)

d. kill $((ps -ef | grep nginx | awk ‘{print $2}’))

3. Your script automates the creation of a virtual machine and you have read the desired size of memory, in gigabytes, into a variable called MEM. The tool you are using inside your script to create the virtual machine expects this value in megabytes. How can you convert MEM to megabytes?

a. MEM=$((MEM * 1024))

b. MEM=$MEM*1024

c. MEM=`$MEM * 1024`

d. MEM=eval($MEM*1024)

4. You are writing a shell script that calls another program called /bin/foo. If the program does not return successfully, you should print an error to the screen. How can you test for an error condition?

a. if [ -e /bin/foo ]

b. if [ $? -gt 0 ]

c. if [ $? -eq 0 ]

d. until [ /bin/foo ]

5. You are looking at an old script you wrote and see this line:

if [ -x /bin/ps -a -f /etc/app.conf ]

In plain English, what does it say?

a. if /bin/ps is excluded from /etc/app.conf

b. if the output of /bin/ps contains all the strings from file /etc/app.conf

c. if /bin/ps is executable and /etc/app.conf exists

d. if either /bin/ps or /etc/app.conf exists

6. Looking inside a script, you see this line:

if [[ `hostname` = 'bob' ]];

What is it doing?

a. Nothing. It is trying to perform an integer comparison on a string.

b. Checking to see whether the hostname command was successful.

c. Changing the hostname to bob and checking to see whether that was successful.

d. Checking to see whether the hostname is bob.

7. If you had an operation you wanted to perform on every process currently running, the most appropriate loop construct would be

a. seq

b. while

c. for

d. until

8. You are writing a shell script that accepts parameters on the command line. How can you check to make sure you’ve received exactly three parameters?

a. if [[ $# -ge 3 ]]

b. if [[ $ARGV = 3 ]]

c. if [[ $? = 3 ]]

d. if [[ 3 -eq $# ]]

9. When writing a bash script, you find yourself needing to exit early because of an error condition. Which of the following commands should be used?

a. die

b. exit 1

c. raise

d. exit

10. Consider the following program, which is run as ./script a b c d.

shift
echo $0 $1

What will it print?

a. b c

b. a b

c. /script b

d. /script a