Linux Command
Linux Command
Linux Command
Before we get to writing new scripts, I want to point out that you have some scripts of your own already. These scripts
were put into your home directory when your account was created, and are used to configure the behavior of your
sessions on the computer. You can edit these scripts to change things.
In this lesson, we will look at a couple of these scripts and learn a few important new concepts about the shell.
Up to now, we really have not discussed exactly what commands are. Commands can be several different things.
Some commands are built into the shell itself. That is, the shell automatically understands a few commands on
its own. The commandscd and pwd are in this group. Commands implemented in the shell itself are called shell
builtins. To see a list of the commands built into bash, use the helpcommand.
The second type of commands is the executable programs. Most commands are in this group. Executable
programs are all the files in the directories included in your path.
The last two groups of commands are contained in your runtime environment. During your session, the system
is holding a number of facts about the world in its memory. This information is called theenvironment. The
environment contains such things as your path, your user name, the name of the file where your mail is
delivered, and much more. You can see a complete list of what is in your environment with the setcommand.
The two types of commands contained in the environment are aliases and shell functions.
Aliases
Now, before you become too confused about what I just said, let's make an alias. Make sure you are in your
home directory. Using your favorite text editor, open the file .bash_profile and add this line to the end of the
file:
The .bash_profile file is a shell script that is executed each time you log in. By adding the alias command to the
file, we have created a new command called "l" which will perform "ls -l". To try out your new command, log out
and log back in. Using this technique, you can create any number of custom commands for yourself. Here is
another one for you to try:
By the way, the aliascommand is just another shell builtin. You can create your aliases directly at the command
prompt; however they will only remain in effect during your current shell session. For example:
Shell functions
Aliases are good for very simple commands, but if you want to create something more complex, you should try
shell functions. Shell functions can be thought of as "scripts within scripts" or little sub-scripts. Let's try one.
Open .bash_profile with your text editor again and replace the alias for "today" with the following:
function today {
Believe it or not, function is a shell builtin too, and as with alias, you can enter shell functions directly at the
command prompt.
>}
[me@linuxbox me]$
type
Since there are many types of commands, it can become confusing to tell what is an alias, a shell function or an
executable file. To determine what a command is, use the typecommand. type will display what type of
command it is. It can be used as follows:
.bashrc
Though placing your aliases and shell functions in your .bash_profile will work, it is not considered good form.
There is a separate file named .bashrc that is intended to be used for such things. You may notice a piece of
code near the beginning of your .bash_profilethat looks something like this:
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
This script fragment checks to see if there is a.bashrc file in your home directory. If one is found, then the script
will read its contents. If this code is in your.bash_profile, you should edit the.bashrc file and put your aliases and
shell functions there.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Here Scripts
In the following lessons, we will construct a useful application. This application will produce an HTML document
that contains information about your system. I spent a lot of time thinking about how to teach shell
programming, and the approach I have come up with is very different from most approaches that I have seen.
Most favor a rather systematic treatment of the many features, and often presume experience with other
programming languages. Although I do not assume that you already know how to program, I realize that many
people today know how to write HTML, so our first program will make a web page. As we construct our script,
we will discover step by step the tools needed to solve the problem at hand.
As you may know, a well formed HTML file contains the following content:
<HTML>
<HEAD>
<TITLE>
</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
Now, with what we already know, we could write a script to produce the above content:
#!/bin/bash
echo "<HTML>"
echo "<HEAD>"
echo "</HEAD>"
echo ""
echo "<BODY>"
echo "</BODY>"
echo "</HTML>"
It has been said that the greatest programmers are also the laziest. They write programs to save themselves
work. Likewise, when clever programmers write programs, they try to save themselves typing.
The first improvement to this script will be to replace the repeated use of the echo command with ahere script,
thusly:
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
_EOF_
A here script (also sometimes called a here document) is an additional form of I/O redirection. It provides a way
to include content that will be given to the standard input of a command. In the case of the script above, the cat
command was given a stream of input from our script to its standard input.
token
token can be any string of characters. I use "_EOF_" (EOF is short for "End Of File") because it is traditional, but
you can use anything, as long as it does not conflict with a bash reserved word. The token that ends the here
script must exactly match the one that starts it, or else the remainder of your script will be interpreted as more
standard input to the command.
There is one additional trick that can be used with a here script. Often you will want to indent the content
portion of the here script to improve the readability of your script. You can do this if you change the script as
follows:
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
</TITLE>
</HEAD>
<BODY>
Your page content goes here.
</BODY>
</HTML>
_EOF_
Changing the the "<<" to "<<-" causes bash to ignore the leading tabs (but not spaces) in the here script. The
output from the cat command will not contain any of the leading tab characters.
O.k., let's make our page. We will edit our page to get it to say something:
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
My System Information
</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
_EOF_
In our next lesson, we will make our script produce real information about the system.
Previous | Contents | Top | Next
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Substitutions - Part 1
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
My System Information
</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
_EOF_
Now that we have our script working, let's improve it. First off, we'll make some changes because we want to be
lazy. In the script above, we see that the phrase "My System Information" is repeated. This is wasted typing (and
extra work!) so we improve it like this:
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
$title
</TITLE>
</HEAD>
<BODY>
<H1>$title</H1>
</BODY>
</HTML>
_EOF_
As you can see, we added a line to the beginning of the script and replaced the two occurrences of the phrase
"My System Information" with$title.
Variables
What we have done is to introduce a very fundamental idea that appears in almost every programming
language, variables. Variables are areas of memory that can be used to store information and are referred to by
a name. In the case of our script, we created a variable called "title" and placed the phrase "My System
Information" into memory. Inside the here script that contains our HTML, we use "$title" to tell the shell to
substitute the contents of the variable.
As we shall see, the shell performs various kinds of substitutions as it processes commands. Wildcards are an
example. When the shell reads a line containing a wildcard, it expands the meaning of the wildcard and then
continues processing the command line. To see this in action, try this:
Variables are treated in much the same way by the shell. Whenever the shell sees a word that begins with a "$",
it tries to find out what was assigned to the variable and substitutes it.
To create a variable, put a line in your script that contains the name of the variable followed immediately by an
equal sign ("="). No spaces are allowed. After the equal sign, assign the information you wish to store. Note that
no spaces are allowed on either side of the equal sign.
You make it up. That's right; you get to choose the names for your variables. There are a few rules.
4. Don't use a name that is already a word understood by bash. These are called reserved words and
should not be used as variable names. If you use one of these words, bash will get confused. To see a list of
reserved words, use the help command.
The addition of the title variable made our life easier in two ways. First, it reduced the amount of typing we had
to do. Second and more important, it made our script easier to maintain.
As you write more and more scripts (or do any other kind of programming), you will learn that programs are
rarely ever finished. They are modified and improved by their creators and others. After all, that's what open
source development is all about. Let's say that you wanted to change the phrase "My System Information" to
"Linuxbox System Information." In the previous version of the script, you would have had to change this in two
locations. In the new version with the title variable, you only have to change it in one place. Since our script is so
small, this might seem like a trivial matter, but as scripts get larger and more complicated, it becomes very
important. Take a look at some of the scripts in the Script Library to get a sense of what large scripts look like.
Environment Variables
When you start your shell session, some variables are already ready for your use. They are defined in scripts that
run each time a user logs in. To see all the variables that are in your environment, use the printenvcommand.
One variable in your environment contains the host name for your system. We will add this variable to our script
like so:
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
$title $HOSTNAME
</TITLE>
</HEAD>
<BODY>
<H1>$title $HOSTNAME</H1>
</BODY>
</HTML>
_EOF_
Now our script will always include the name of the machine on which we are running. Note that, by convention,
environment variables names are uppercase.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Substitutions - Part 2
In our last lesson, we learned how to create variables and perform substitutions with them. In this lesson, we
will extend this idea to show how we can substitute the results from a command.
When we last left our script, it could create an HTML page that contained a few simple lines of text, including
the host name of the machine which we obtained from the environment variable HOSTNAME. Next, we will add
a timestamp to the page to indicate when it was last updated, along with the user that did it.
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
$title $HOSTNAME
</TITLE>
</HEAD>
<BODY>
<H1>$title $HOSTNAME</H1>
</BODY>
</HTML>
_EOF_
As you can see, we employed another environment variable, USER, to get the user name. In addition, we used
this strange looking thing:
The characters "$( )" tell the shell, "substitute the results of the enclosed command." In our script, we want the
shell to insert the results of the command date +"%x %r %Z"which expresses the current date and time. The date
command has many features and formatting options. To look at them all, try this:
Be aware that there is an older, alternate syntax for "$(command)" that uses the backtick character " ` ". This
older form is compatible with the original Bourne shell (sh). I tend not to use the older form since I am teaching
bash here, not sh, and besides, I think backticks are ugly. The bash shell fully supports scripts written for sh, so
the following forms are equivalent:
$(command)
`command`
The first thing to try is "--help". All of the tools written by the GNU Project from the Free Software Foundation
implement this option. To get a brief list of the command's options, just type:
Many commands (besides the GNU tools) will either accept the --help option or will consider it an invalid option
and will display a usage message which you may find equally useful.
If the results of the --help option scroll off the screen, pipe the results into less to view it like this:
Some commands don't have help messages or don't use --help to invoke them. On these mysterious commands,
I use this trick:
First, find out where the executable file is located (this trick will only work with programs, not shell builtins). This
is easily done by typing:
The which command will tell you the path and file name of the executable program. Next, use the strings
command to display text that may be embedded within the executable file. For example, if you wanted to look
inside the bash program, you would do the following:
/bin/bash
The strings command will display any human readable content buried inside the program. This might include
copyright notices, error messages, help text, etc.
Finally, if you have a very inquisitive nature, get the command's source code and read that. Even if you cannot
fully understand the programming language in which the command is written, you may be able to gain valuable
insight by reading the author's comments in the program's source.
You can even nest the variables (place one inside another), like this:
As the name variable suggests, the content of a variable is subject to change. This means that it is expected that
during the execution of your script, a variable may have its content modified by something you do.
On the other hand, there may be values that, once set, should never be changed. These are calledconstants. I
bring this up because it is a common idea in programming. Most programming languages have special facilities
to support values that are not allowed to change. Bash also has these facilities but, to be honest, I never see it
used. Instead, if a value is intended to be a constant, it is simply given an uppercase name. Environment
variables are usually considered constants since they are rarely changed. Like constants, environment variables
are given uppercase names by convention. In the scripts that follow, I will use this convention - uppercase
names for constants and lowercase names for variables.
#!/bin/bash
<HTML>
<HEAD>
<TITLE>
$TITLE
</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIME_STAMP
</BODY>
</HTML>
_EOF_
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Quoting
We are going to take a break from our script to discuss something we have been doing but have not explained
yet. In this lesson we will coverquoting. Quoting is used to accomplish two goals:
We have already used quoting. In our script, the assignment of text to our constants was performed with
quoting:
In this case, the text is surrounded by double quote characters. The reason we use quoting is to group the words
together. If we did not use quotes, bash would think all of the words after the first one were additional
commands. Try this:
The shell recognizes both single and double quote characters. The following are equivalent:
However, there is an important difference between single and double quotes. Single quotes limit substitution.
As we saw in the previous lesson, you can place variables in double quoted text and the shell still performs
substitution. We can see this with the echocommand:
Double quotes do not suppress the substitution of words that begin with "$" but they do suppress the expansion
of wildcard characters. For example, try the following:
There is another quoting character you will encounter. It is the backslash. The backslash tells the shell to "ignore
the next character." Here is an example:
By using the backslash, the shell ignored the "$" symbol. Since the shell ignored it, it did not perform the
substitution on $HOSTNAME. Here is a more useful example:
[me@linuxbox me]$ echo "My host name is \"$HOSTNAME\"."
As you can see, using the \" sequence allows us to embed double quotes into our text.
If you look at the manpages for any program written by the GNU project, you will notice that in addition to
command line options consisting of a dash and a single letter, there are also long option names that begin with
two dashes. For example, the following are equivalent:
ls -r
ls --reverse
Why do they support both? The short form is for lazy typists on the command line and the long form is for
scripts. I sometimes use obscure options, and I find the long form useful if I have to review my script again
months after I wrote it. Seeing the long form helps me understand what the option does, saving me a trip to the
manpage. A little more typing now, a lot less work later. Laziness is maintained.
As you might suspect, using the long form options can make a single command line very long. To combat this
problem, you can use a backslash to get the shell to ignore a newline character like this:
ls -l \
--reverse \
--human-readable \
--full-time
Using the backslash in this way allows us to embed newlines in our command. Note that for this trick to work,
the newline must be typed immediately after the backslash. If you put a space after the backslash, the space will
be ignored, not the newline. Backslashes are also used to insert special characters into our text. These are
calledbackslash escape characters. Here are the common ones:
DEL C:\WIN2K\LEGACY_OS.EXE
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
• Who, What, Where, Why
________________________________________
Shell Functions
As programs get longer and more complex, they become more difficult to design, code, and maintain. As with
any large endeavor, it is often useful to break a single, large task into a number of smaller tasks.
In this lesson, we will begin to break our single monolithic script into a number of separate functions.
To get familiar with this idea, let's consider the description of an everyday task -- going to the market to buy
food. Imagine that we were going to describe the task to a man from Mars.
1. Leave house
2. Drive to market
3. Park car
4. Enter market
5. Purchase food
6. Drive home
7. Park car
8. Enter house
This description covers the overall process of going to the market; however a man from Mars will probably
require additional detail. For example, the "Park car" sub task could be described as follows:
5. Exit car
6. Lock car
Of course the task "Turn off motor" has a number of steps such as "turn off ignition" and "remove key from
ignition switch," and so on.
This process of identifying the top-level steps and developing increasingly detailed views of those steps is called
top-down design. This technique allows you to break large complex tasks into many small, simple tasks.
As our script continues to grow, we will use top down design to help us plan and code our script.
1. Open page
3. Write title
6. Write title
9. Close page
All of these tasks are implemented, but we want to add more. Let's insert some additional tasks after task 7:
9. Write up-time
It would be great if there were commands that performed these additional tasks. If there were, we could use
command substitution to place them in our script like so:
#!/bin/bash
##### Constants
TITLE="System Information for $HOSTNAME"
##### Main
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIME_STAMP</p>
$(system_info)
$(show_uptime)
$(drive_space)
$(home_space)
</body>
</html>
_EOF_
While there are no commands that do exactly what we need, we can create them using shell functions.
As we learned in lesson 2, shell functions act as "little programs within programs" and allow us to follow top-
down design principles. To add the shell functions to our script, we change it so:
#!/bin/bash
# system_page - A script to produce an system information HTML file
##### Constants
##### Functions
function system_info
function show_uptime
function drive_space
}
function home_space
##### Main
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIME_STAMP</p>
$(system_info)
$(show_uptime)
$(drive_space)
$(home_space)
</body>
</html>
_EOF_
A couple of important points about functions: First, they must appear before you attempt to use them. Second,
the function body (the portions of the function between the { and } characters) must contain at least one valid
command. As written, the script will not execute without error, because the function bodies are empty. The
simple way to fix this is to place a returnstatement in each function body. After you do this, our script will
execute successfully again.
When you are developing a program, it is is often a good practice to add a small amount of code, run the script,
add some more code, run the script, and so on. This way, if you introduce a mistake into your code, it will be
easier to find and correct.
As you add functions to your script, you can also use a technique called stubbing to help watch the logic of your
script develop. Stubbing works like this: imagine that we are going to create a function called "system_info" but
we haven't figured out all of the details of its code yet. Rather than hold up the development of the script until
we are finished with system_info, we just add an echo command like this:
function system_info
This way, our script will still execute sucessfully, even though we do not yet have a finished system_info
function. We will later replace the temporary stubbing code with the complete working version.
The reason we use an echo command is so we get some feedback from the script to indicate that the functions
are being executed.
Let's go ahead and write stubs for our new functions and keep the script working.
#!/bin/bash
##### Constants
##### Functions
function system_info
function show_uptime
function drive_space
function home_space
{
##### Main
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIME_STAMP</p>
$(system_info)
$(show_uptime)
$(drive_space)
$(home_space)
</body>
</html>
_EOF_
Previous | Contents | Top | Next
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
In this lesson, we will develop some of our shell functions and get our script to produce some useful
information.
show_uptime
The show_uptime function will display the output of the uptime command. The uptime command outputs
several interesting facts about the system, including the length of time the system has been "up" (running) since
its last re-boot, the number of users and recent system load.
To get the output of the uptime command into our HTML page, we will code our shell function like this,
replacing our temporary stubbing code with the finished version:
function show_uptime
uptime
echo "</pre>"
As you can see, this function outputs a stream of text containing a mixture of HTML tags and command output.
When the substitution takes place in the main body of the our program, the output from our function becomes
part of the here script.
drive_space
The drive_space function will use the dfcommand to provide a summary of the space used by all of the mounted
file systems.
[me@linuxbox me]$ df
In terms of structure, the drive_space function is very similar to the show_uptime function:
function drive_space
echo "<pre>"
df
echo "</pre>"
home_space
The home_space function will display the amount of space each user is using in his/her home directory. It will
display this as a list, sorted in descending order by the amount of space used.
function home_space
echo "<pre>"
echo "</pre>"
Note that in order for this function to successfully execute, the script must be run by the superuser, since the
ducommand requires superuser privileges to examine the contents of the /home directory.
system_info
We're not ready to finish the system_info function yet. In the meantime, we will improve the stubbing code so it
produces valid HTML:
function system_info
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• Script library
• SuperMan pages
________________________________________
In this lesson, we will look at how to add intelligence to our scripts. So far, our script has only consisted of a
sequence of commands that starts at the first line and continues line by line until it reaches the end. Most
programs do more than this. They make decisions and perform different actions depending onconditions.
The shell provides several commands that we can use to control the flow of execution in our program. These
include:
• if
• exit
• for
• while
• until
• case
• break
• continue
if
The first command we will look at is if. The ifcommand is fairly simple on the surface; it makes a decision based
on a condition. The if command has three forms:
# First form
if condition ; then
commands
fi
# Second form
if condition ; then
commands
else
commands
fi
# Third form
if condition ; then
commands
commands
fi
In the first form, if the condition is true, then commands are performed. If the condition is false, nothing is done.
In the second form, if the condition is true, then the first set of commands is performed. If the condition is false,
the second set of commands is performed.
In the third form, if the condition is true, then the first set of commands is performed. If the condition is false,
and if the second condition is true, then the second set of commands is performed.
What is a "condition"?
To be honest, it took me a long time to really understand how this worked. To help answer this, there is yet
another basic behavior of commands we must discuss.
Exit status
A properly written Unix application will tell the operating system if it was successful or not. It does this by means
of an exit status. The exit status is a numeric value in the range of 0 to 255. A "0" indicates success; any other
value indicates failure. Exit status provides two important features. First, it can be used to detect and handle
errors and second, it can be used to perform true/false tests.
It is easy to see that handling errors would be valuable. For example, in our script we will want to look at what
kind of hardware is installed so we can include it in our report. Typically, we will try to query the hardware, and
if an error is reported by whatever tool we use to do the query, our script will be able to skip the portion of the
script which deals with the missing hardware.
We can also use the exit status to perform simple true/false decisions. We will cover this next.
test
The test command is used most often with the ifcommand to perform true/false decisions. The command is
unusual in that it has two different syntactic forms:
# First form
test expression
# Second form
[ expression ]
The test command works simply. If the given expression is true, test exits with a status of zero; otherwise it exits
with a status of 1.
The neat feature of testis the variety of expressions you can create. Here is an example:
if [ -f .bash_profile ]; then
else
fi
In this example, we use the expression " -f .bash_profile ". This expression asks, "Is .bash_profile a file?" If the
expression is true, then testexits with a zero (indicating true) and the if command executes the command(s)
following the word then. If the expression is false, thentest exits with a status of one and the if command
executes the command(s) following the word else.
Here is a partial list of the conditions thattest can evaluate. Since test is a shell builtin, use "help test" to see a
complete list.
Expression Description
file1 -nt file2 True if file1 is newer than (according to modification time) file2
Before we go on, I want to explain the rest of the example above, since it also reveals more important ideas.
In the first line of the script, we see the if command followed by the test command, followed by a semicolon,
and finally the word then. I chose to use the [ expression ] form of the test command since most people think it's
easier to read. Notice that the spaces between the "[" and the beginning of the expression are required.
Likewise, the space between the end of the expression and the trailing "]".
The semicolon is a command separator. Using it allows you to put more than one command on a line. For
example:
I use the semicolon as I did to allow me to put the word then on the same line as the if command, because I
think it is easier to read that way.
On the second line, there is our old friend echo. The only thing of note on this line is the indentation. Again for
the benefit of readability, it is traditional to indent all blocks of conditional code; that is, any code that will only
be executed if certain conditions are met. The shell does not require this; it is done to make the code easier to
read.
In other words, we could write the following and get the same results:
# Alternate form
if [ -f .bash_profile ]
then
else
fi
if [ -f .bash_profile ]
fi
exit
In order to be good script writers, we must set the exit status when our scripts finish. To do this, use the exit
command. The exit command causes the script to terminate immediately and set the exit status to whatever
value is given as an argument. For example:
exit 0
exits your script and sets the exit status to 0 (success), whereas
exit 1
exits your script and sets the exit status to 1 (failure).
When we last left our script, we required that it be run with superuser privileges. This is because the
home_space function needs to examine the size of each user's home directory, and only the superuser is
allowed to do that.
But what happens if a regular user runs our script? It produces a lot of ugly error messages. What if we could put
something in the script to stop it if a regular user attempts to run it?
The id command can tell us who the current user is. When executed with the "-u" option, it prints the numeric
user id of the current user.
[me@linuxbox me]$ id -u
501
[me@linuxbox me]$ su
Password:
[root@linuxbox me]# id -u
If the superuser executes id -u, the command will output "0." This fact can be the basis of our test:
echo "superuser"
fi
In this example, if the output of the commandid -u is equal to the string "0", then print the string "superuser."
While this code will detect if the user is the superuser, it does not really solve the problem yet. We want to stop
the script if the user is not the superuser, so we will code it like so:
exit 1
fi
With this code, if the output of the id -u command is not equal to "0", then the script prints a descriptive error
message, exits, and sets the exit status to 1, indicating to the operating system that the script executed
unsuccessfully.
Notice the ">&2" at the end of the echo command. This is another form of I/O direction. You will often notice
this in routines that display error messages. If this redirection were not done, the error message would go to
standard output. With this redirection, the message is sent to standard error. Since we are executing our script
and redirecting its standard output to a file, we want the error messages separated from the normal output.
We could put this routine near the beginning of our script so it has a chance to detect a possible error before
things get under way, but in order to run this script as an ordinary user, we will use the same idea and modify
the home_spacefunction to test for proper privileges instead, like so:
function home_space
echo "<pre>"
echo "</pre>"
fi
} # end of home_space
This way, if an ordinary user runs the script, the troublesome code will be passed over, rather than executed and
the problem will be solved.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
Linux® is a registered trademark of Linus Torvalds.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Now that our scripts are getting a little more complicated, I want to point out some common mistakes that you
might run into. To do this, create the following script called trouble.bash. Be sure to enter it exactly as written.
#!/bin/bash
number=1
else
fi
When you run this script, it should output the line "Number equals 1" because, well, number equals 1. If you
don't get the expected output, check your typing; you made a mistake.
Empty variables
Edit the script to change line 3 from:
number=1
to:
number=
and run the script again. This time you should get the following:
As you can see, bashdisplayed an error message when we ran the script. You probably think that by removing
the "1" on line 3 it created a syntax error on line 3, but it didn't. Let's look at the error message again:
We can see that ./trouble.bash is reporting the error and the error has to do with "[". Remember that "[" is an
abbreviation for the test shell builtin. From this we can determine that the error is occurring on line 5 not line 3.
First, let me say there is nothing wrong with line 3. number= is perfectly good syntax. You will sometimes want
to set a variable's value to nothing. You can confirm the validity of this by trying it on the command line:
[me@linuxbox me]$
To understand this error, we have to see what the shell sees. Remember that the shell spends a lot of its life
substituting text. In line 5, the shell substitutes the value of number where it sees $number. In our first try
(when number=1), the shell substituted 1 for $number like so:
if [ 1 = "1" ]; then
However, when we set number to nothing (number=), the shell saw this after the substitution:
if [ = "1" ]; then
which is an error. It also explains the rest of the error message we received. The "=" is a binary operator; that is,
it expects two items to operate upon - one on each side. What the shell was trying to tell us was that there was
only one item and there should have been a unary operator (like "!") that only operates on a single item.
This brings up an important thing to remember when you are writing your scripts. Consider what happens if a
variable is set to equal nothing.
Missing quotes
Edit line 6 to remove the trailing quote from the end of the line:
Here we have another case of a mistake in one line causing a problem later in the script. What happens is the
shell keeps looking for the closing quotation mark to tell it where the end of the string is, but runs into the end
of the file before it finds it.
These errors can be a real pain to find in a long script. This is one reason you should test your scripts frequently
when you are writing them so there is less new code to test. I also find that text editors with syntax highlighting
(like nedit or kate) make these kinds of bugs easier to find.
Isolating problems
Finding bugs in your programs can sometimes be very difficult and frustrating. Here are a couple of techniques
that you will find useful:
Isolate blocks of code by "commenting them out." This trick involves putting comment characters at the
beginning of lines of code to stop the shell from reading them. Frequently, you will do this to a block of code to
see if a particular problem goes away. By doing this, you can isolate which part of a program is causing (or not
causing) a problem.
For example, when we were looking for our missing quotation we could have done this:
#!/bin/bash
number=1
#else
fi
By commenting out the elseclause and running the script, we could show that the problem was not in the else
clause even though the error message suggested that it was.
Use echo commands to verify your assumptions. As you gain experience tracking down bugs, you will discover
that bugs are often not where you first expect to find them. A common problem will be that you will make a
false assumption about the performance of your program. You will see a problem develop at a certain point in
your program and assume that the problem is there. This is often incorrect, as we have seen. To combat this,
you should place echo commands in your code while you are debugging, to produce messages that confirm the
program is doing what is expected. There are two kinds of messages that you should insert.
The first type simply announces that you have reached a certain point in the program. We saw this in our earlier
discussion on stubbing. It is useful to know that program flow is happening the way we expect.
The second type displays the value of a variable (or variables) used in a calculation or test. You will often find
that a portion of your program will fail because something that you assumed was correct earlier in your program
is, in fact, incorrect and is causing your program to fail later on.
It is possible to have bash show you what it is doing when you run your script. To do this, add a "-x" to the first
line of your script, like this:
#!/bin/bash -x
Now, when you run your script, bash will display each line (with substitutions performed) as it executes it. This
technique is calledtracing. Here is what it looks like:
+ number=1
+ '[' 1 = 1 ']'
Number equals 1
Alternately, you can use the set command within your script to turn tracing on and off. Use set -x to turn tracing
on and set +x to turn tracing off. For example.:
#!/bin/bash
number=1
set -x
else
fi
set +x
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Learning the shell
• Script library
• SuperMan pages
________________________________________
Up to now, our scripts have not been interactive. That is, they did not require any input from the user. In this
lesson, we will see how your scripts can ask questions, and get and use responses.
read
To get input from the keyboard, you use the read command. The read command takes input from the keyboard
and assigns it to a variable. Here is an example:
#!/bin/bash
read text
As you can see, we displayed a prompt on line 3. Note that "-n" given to the echo command causes it to keep
the cursor on the same line; i.e., it does not output a carriage return at the end of the prompt.
Next, we invoke the readcommand with "text" as its argument. What this does is wait for the user to type
something followed by a carriage return (the Enter key) and then assign whatever was typed to the variabletext.
The read command also takes some command line options. The two most interesting ones are -t and -s. The -t
option followed by a number of seconds provides an automatic timeout for the read command. This means that
theread command will give up after the specified number of seconds if no response has been received from the
user. This option could be used in the case of a script that must continue (perhaps resorting to a default
response) even if the user does not answer the prompts. Here is the -t option in action:
#!/bin/bash
else
fi
The -s option causes the user's typing not to be displayed. This is useful when you are asking the user to type in
a password or other security related information.
Arithmetic
Since we are working on a computer, it is natural to expect that it can perform some simple arithmetic. The shell
provides features forinteger arithmetic.
What's an integer? That means whole numbers like 1, 2, 458, -2859. It does not mean fractional numbers like
0.5, .333, or 3.1415. If you must deal with fractional numbers, there is a separate program called bc which
provides an arbitrary precision calculator language. It can be used in shell scripts, but is beyond the scope of this
tutorial.
Let's say you want to use the command line as a primitive calculator. You can do it like this:
As you can see, when you surround an arithmetic expression with the double parentheses, the shell will perform
arithmetic evaluation.
4
[me@linuxbox me]$ echo $(( 2+2 ))
The shell can perform a variety of common (and not so common) arithmetic operations. Here is an example:
#!/bin/bash
first_num=0
second_num=0
read first_num
read second_num
Notice how the leading "$" is not needed to reference variables inside the arithmetic expression such as
"first_num + second_num".
Try this program out and watch how it handles division (remember this is integer division) and how it handles
large numbers. Numbers that get too large overflow like the odometer in a car when you exceed the number of
miles it was designed to count. It starts over but first it goes through all the negative numbers because of how
integers are represented in memory. Division by zero (which is mathematically invalid) does cause an error.
I'm sure that you recognize the first four operations as addition, subtraction, multiplication and division, but that
the fifth one may be unfamiliar. The "%" symbol represents remainder (also known as modulo). This operation
performs division but instead of returning a quotient like division, it returns the remainder. While this might not
seem very useful, it does, in fact, provide great utility when writing programs. For example, when a remainder
operation returns zero, it indicates that the first number is an exact multiple of the second. This can be very
handy:
#!/bin/bash
number=0
read number
else
fi
Or, in this program that formats an arbitrary number of seconds into hours and minutes:
#!/bin/bash
seconds=0
read seconds
hours=$((seconds / 3600))
seconds=$((seconds % 3600))
minutes=$((seconds / 60))
seconds=$((seconds % 60))
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
More branching
In the previous lesson on flow control we learned about the if command and how it is used to alter program flow
based on a condition. In programming terms, this type of program flow is calledbranching because it is like
traversing a tree. You come to a fork in the tree and the evaluation of a condition determines which branch you
take.
There is a second and more complex kind of branching called a case. A case is multiple-choice branch. Unlike the
simple branch, where you take one of two possible paths, a case supports several possible outcomes based on
the evaluation of a condition.
You can construct this type of branch with multiple if statements. In the example below, we evaluate some input
from the user:
#!/bin/bash
read character
else
else
else
fi
fi
fi
Fortunately, the shell provides a more elegant solution to this problem. It provides a built-in command called
case, which can be used to construct an equivalent program:
#!/bin/bash
read character
case $character in
;;
;;
;;
esac
case word in
patterns ) statements ;;
esac
case selectively executes statements if word matches a pattern. You can have any number of patterns and
statements. Patterns can be literal text or wildcards. You can have multiple patterns separated by the "|"
character. Here is a more advanced example to show what I mean:
#!/bin/bash
read character
case $character in
# Check for letters
;;
;;
esac
Notice the special pattern "*". This pattern will match anything, so it is used to catch cases that did not match
previous patterns. Inclusion of this pattern at the end is wise, as it can be used to detect invalid input.
Loops
The final type of program flow control we will discuss is called looping. Looping is repeatedly executing a section
of your program based on a condition. The shell provides three commands for looping: while,until and for. We
are going to cover while and untilin this lesson and for in a future lesson.
The while command causes a block of code to be executed over and over, as long as a condition is true. Here is a
simple example of a program that counts from zero to nine:
#!/bin/bash
number=0
number=$((number + 1))
done
On line 3, we create a variable callednumber and initialize its value to 0. Next, we start the whileloop. As you can
see, we have specified a condition that tests the value of number. In our example, we test to see if number has a
value less than 10.
Notice the word do on line 4 and the word done on line 7. These enclose the block of code that will be repeated
as long as the condition is met.
In most cases, the block of code that repeats must do something that will eventually change the outcome of the
condition, otherwise you will have what is called an endless loop; that is, a loop that never ends.
In the example, the repeating block of code outputs the value of number (the echo command on line 5) and
increments number by one on line 6. Each time the block of code is completed, the condition is tested again.
After the tenth iteration of the loop, number has been incremented ten times and the condition is no longer
true. At that point, the program flow resumes with the statement following the word done. Sincedone is the last
line of our example, the program ends.
The until command works exactly the same way, except the block of code is repeated as long as the condition is
false. In the example below, notice how the condition has been changed from the whileexample to achieve the
same result:
#!/bin/bash
number=0
number=$((number + 1))
done
Building a menu
One common way of presenting a user interface for a text based program is by using a menu. A menu is a list of
choices from which the user can pick.
In the example below, we use our new knowledge of loops and cases to build a simple menu driven application:
#!/bin/bash
selection=
echo ""
echo "PROGRAM MENU"
echo ""
echo ""
read selection
echo ""
case $selection in
1 ) df ;;
2 ) free ;;
0 ) exit ;;
esac
done
The purpose of the untilloop in this program is to re-display the menu each time a selection has been
completed. The loop will continue until selection is equal to "0," the "exit" choice. Notice how we defend against
entries from the user that are not valid choices.
To make this program better looking when it runs, we can enhance it by adding a function that asks the user to
press the Enter key after each selection has been completed, and clears the screen before the menu is displayed
again. Here is the enhanced example:
#!/bin/bash
function press_enter
echo ""
clear
selection=
echo ""
echo ""
echo ""
read selection
echo ""
case $selection in
1 ) df ; press_enter ;;
2 ) free ; press_enter ;;
0 ) exit ;;
esac
done
Imagine this situation: you have an external device attached to your computer, such as a USB disk drive but you
forgot to turn it on. You try and use the device but the application hangs instead. When this happens, you could
picture the following dialog going on between the application and the interface for the device:
Well-written software tries to avoid this situation by instituting a timeout. This means that the loop is also
counting the number of attempts or calculating the amount of time it has waited for something to happen. If the
number of tries or the amount of time allowed is exceeded, the loop exits and the program generates an error
and exits.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Learning the shell
• Script library
• SuperMan pages
________________________________________
Positional Parameters
#!/bin/bash
##### Constants
##### Functions
function system_info
function show_uptime
echo "<pre>"
uptime
echo "</pre>"
} # end of show_uptime
function drive_space
echo "<pre>"
df
echo "</pre>"
} # end of drive_space
function home_space
echo "<pre>"
echo "</pre>"
fi
} # end of home_space
##### Main
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIME_STAMP</p>
$(system_info)
$(show_uptime)
$(drive_space)
$(home_space)
</body>
</html>
_EOF_
We have most things working, but there are several more features I want to add:
1. I want to specify the name of the output file on the command line, as well as set a default output file
name if no name is specified.
2. I want to offer an interactive mode that will prompt for a file name and warn the user if the file exists
and prompt the user to overwrite it.
3. Naturally, we want to have a help option that will display a usage message.
All of these features involve using command line options and arguments. To handle options on the command
line, we use a facility in the shell called positional parameters. Positional parameters are a series of special
variables ($0 through $9) that contain the contents of the command line.
If some_program were a bash shell script, we could read each item on the command line because the positional
parameters contain the following:
#!/bin/bash
Often, you will want to check to see if you have arguments on which to act. There are a couple of ways to do
this. First, you could simply check to see if $1 contains anything like so:
#!/bin/bash
else
fi
Second, the shell maintains a variable called$# that contains the number of items on the command line in
addition to the name of the command ($0).
#!/bin/bash
if [ $# -gt 0 ]; then
else
fi
As we discussed before, many programs, particularly ones from the GNU Project, support both short and long
command line options. For example, to display a help message for many of these programs, you may use either
the "-h" option or the longer "--help" option. Long option names are typically preceded by a double dash. We
will adopt this convention for our scripts.
interactive=
filename=~/system_page.html
case $1 in
-f | --file ) shift
filename=$1
;;
-i | --interactive ) interactive=1
;;
-h | --help ) usage
exit
;;
*) usage
exit 1
esac
shift
done
The first two lines are pretty easy. We set the variable interactive to be empty. This will indicate that the
interactive mode has not been requested. Then we set the variablefilename to contain a default file name. If
nothing else is specified on the command line, this file name will be used.
After these two variables are set, we have default settings, in case the user does not specify any options.
Next, we construct a whileloop that will cycle through all the items on the command line and process each one
with case. The case will detect each possible option and process it accordingly.
Now the tricky part. How does that loop work? It relies on the magic of shift.
shift is a shell builtin that operates on the positional parameters. Each time you invoke shift, it "shifts" all the
positional parameters down by one. $2 becomes $1, $3becomes $2, $4 becomes$3, and so on. Try this:
#!/bin/bash
echo "You start with $# positional parameters"
shift
done
Our "-f" option takes a required argument, a valid file name. We use shift again to get the next item from the
command line and assign it tofilename. Later we will have to check the content of filename to make sure it is
valid.
We will have to move a few things around and add a usage function to get this new routine integrated into our
script. We'll also add some test code to verify that the command line processor is working correctly. Our script
now looks like this:
#!/bin/bash
##### Constants
##### Functions
function system_info
} # end of system_info
function show_uptime
echo "<pre>"
uptime
echo "</pre>"
} # end of show_uptime
function drive_space
echo "<pre>"
df
echo "</pre>"
} # end of drive_space
function home_space
echo "<pre>"
echo "</pre>"
fi
} # end of home_space
function write_page
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIME_STAMP</p>
$(system_info)
$(show_uptime)
$(drive_space)
$(home_space)
</body>
</html>
_EOF_
function usage
##### Main
interactive=
filename=~/system_page.html
case $1 in
-f | --file ) shift
filename=$1
;;
-i | --interactive ) interactive=1
;;
-h | --help ) usage
exit
;;
*) usage
exit 1
esac
shift
done
else
fi
response=
read response
if [ -n "$response" ]; then
filename=$response
fi
if [ -f $filename ]; then
read response
exit 1
fi
fi
fi
First, we check if the interactive mode is on, otherwise we don't have anything to do. Next, we ask the user for
the file name. Notice the way the prompt is worded:
After we have the name of the output file, we check if it already exists. If it does, we prompt the user. If the user
response is not "y," we give up and exit, otherwise we can proceed.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Now that you have learned about positional parameters, it is time to cover the remaining flow control
statement, for. Likewhile and until, for is used to construct loops. forworks like this:
statements
done
In essence, for assigns a word from the list of words to the specified variable, executes the statements, and
repeats this over and over until all the words have been used up. Here is an example:
#!/bin/bash
echo $i
done
In this example, the variable i is assigned the string "word1", then the statement echo $i is executed, then the
variable i is assigned the string "word2", and the statement echo $iis executed, and so on, until all the words in
the list of words have been assigned.
The interesting thing about for is the many ways you can construct the list of words. All kinds of substitutions
can be used. In the next example, we will construct the list of words from a command:
#!/bin/bash
count=0
count=$((count + 1))
done
Here we take the file .bash_profile and count the number of words in the file and the number of characters in
each word.
So what's this got to do with positional parameters? Well, one of the features of for is that it can use the
positional parameters as the list of words:
#!/bin/bash
for i in $@; do
echo $i
done
The shell variable $@ contains the list of command line arguments. This technique is a very common approach
to processing a list of files on the command line. Here is a another example:
#!/bin/bash
result=
if [ -f $filename ]; then
else
if [ -d $filename ]; then
result="$filename is a directory"
fi
fi
if [ -w $filename ]; then
else
fi
echo "$result"
done
Try this script. Give it a list of files or a wildcard like "*" to see it work.
Here is another example script. This one compares the files in two directories and lists which files in the first
directory are missing from the second.
#!/bin/bash
exit 1
fi
if [ ! -d $1 ]; then
exit 1
fi
if [ ! -d $2 ]; then
exit 1
fi
missing=0
fn=$(basename "$filename")
if [ -f "$filename" ]; then
if [ ! -f "$2/$fn" ]; then
missing=$((missing + 1))
fi
fi
done
echo "$missing files missing"
Now on to the real work. We are going to improve the home_space function in our script to output more
information. You will recall that our previous version looked like this:
function home_space
echo "<pre>"
echo "</pre>"
fi
} # end of home_space
function home_space
echo "<pre>"
format="%8s%10s%10s %-s\n"
dir_list="/home/*"
else
dir_list=$HOME
fi
total_blocks=$(du -s $home_dir)
done
echo "</pre>"
} # end of home_space
This improved version introduces a new commandprintf, which is used to produce formatted output according
to the contents of a format string. printf comes from the C programming language and has been implemented in
many other programming languages including C++, perl, awk, java, PHP, and of course, bash. You can read more
about printf format strings at:
We also introduce the find command. find is used to search for files or directories that meet specific criteria. In
thehome_space function, we use find to list the directories and regular files in each home directory. Using the
wc command, we count the number of files and directories found.
The really interesting thing abouthome_space is how we deal with the problem of superuser access. You will
notice that we test for the superuser with id and, according to the outcome of the test, we assign different
strings to the variabledir_list, which becomes the list of words for the for loop that follows. This way, if an
ordinary user runs the script, only his/her home directory will be listed.
Another function that can use a for loop is our unfinishedsystem_info function. We can build it like this:
function system_info
echo "<pre>"
for i in /etc/*release; do
head -n 1 $i
done
uname -orp
echo "</pre>"
fi
} # end of system_info
In this function, we first determine if there are any release files to process. The release files contain the name of
the vendor and the version of the distribution. They are located in the/etc directory. To detect them, we
perform an ls command and throw away all of its output. We are only interested in the exit status. It will be true
if any files are found.
Next, we output the HTML for this section of the page, since we now know that there are release files to
process. To process the files, we start a for loop to act on each one. Inside the loop, we use the head command
to return the first line of each file.
Finally, we use the uname command with the "o", "r", and "p" options to obtain some additional information
from the system.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• Script library
• SuperMan pages
________________________________________
In this lesson, we're going to look at handling errors during the execution of your scripts.
The difference between a good program and a poor one is often measured in terms of the program's
robustness. That is, the program's ability to handle situations in which something goes wrong.
Exit status
As you recall from previous lessons, every well-written program returns an exit status when it finishes. If a
program finishes successfully, the exit status will be zero. If the exit status is anything other than zero, then the
program failed in some way.
It is very important to check the exit status of programs you call in your scripts. It is also important that your
scripts return a meaningful exit status when they finish. I once had a Unix system administrator who wrote a
script for a production system containing the following 2 lines of code:
cd $some_directory
rm *
Why is this such a bad way of doing it? It's not, if nothing goes wrong. The two lines change the working
directory to the name contained in $some_directory and delete the files in that directory. That's the intended
behavior. But what happens if the directory named in$some_directory doesn't exist? In that case, the
cdcommand will fail and the script executes the rm command on the current working directory. Not the
intended behavior!
By the way, my hapless system administrator's script suffered this very failure and it destroyed a large portion of
an important production system. Don't let this happen to you!
The problem with the script was that it did not check the exit status of the cd command before proceeding with
therm command.
There are several ways you can get and respond to the exit status of a program. First, you can examine the
contents of the $?environment variable. $? will contain the exit status of the last command executed. You can
see this work with the following:
The true and false commands are programs that do nothing except return an exit status of zero and one,
respectively. Using them, we can see how the $? environment variable contains the exit status of the previous
program.
So to check the exit status, we could write the script this way:
cd $some_directory
rm *
else
exit 1
fi
In this version, we examine the exit status of the cd command and if it's not zero, we print an error message on
standard error and terminate the script with an exit status of 1.
While this is a working solution to the problem, there are more clever methods that will save us some typing.
The next approach we can try is to use the if statement directly, since it evaluates the exit status of commands it
is given.
# A better way
if cd $some_directory; then
rm *
else
exit 1
fi
Here we check to see if the cd command is successful. Only then does rm get executed; otherwise an error
message is output and the program exits with a code of 1, indicating that an error has occurred.
Since we will be checking for errors often in our programs, it makes sense to write a function that will display
error messages. This will save more typing and promote laziness.
function error_exit
exit 1
# Using error_exit
if cd $some_directory; then
rm *
else
fi
Finally, we can further simplify our script by using the AND and OR control operators. To explain how they work,
I will quote from the bash man page:
"The control operators && and || denote AND lists and OR lists, respectively. An AND list has the form
command2 is executed if, and only if, command1returns an exit status of zero.
command1 || command2
command2 is executed if, and only if, command1returns a non-zero exit status. The return status of AND and OR
lists is the exit status of the last command executed in the list."
Again, we can use the true and false commands to see this work:
echo executed
echo executed
[me] $
# Simplest of all
rm *
If an exit is not required in case of error, then you can even do this:
I want to point out that even with the defense against errors we have introduced in our example for the use of
cd, this code is still vulnerable to a common programming error, namely, what happens if the name of the
variable containing the name of the directory is misspelled? In that case, the shell will interpret the variable as
empty and the cd succeed, but it will change directories to the user's home directory, so beware!
There are a number of improvements that we can make to theerror_exit function. I like to include the name of
the program in the error message to make clear where the error is coming from. This becomes more important
as your programs get more complex and you start having scripts launching other scripts, etc. Also, note the
inclusion of the LINENO environment variable which will help you identify the exact line within your script where
the error occurred.
#!/bin/bash
# holds the name of the program being run. You can get this
PROGNAME=$(basename $0)
function error_exit
# ----------------------------------------------------------------
# Accepts 1 argument:
# ----------------------------------------------------------------
echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
exit 1
# line number.
The use of the curly braces within the error_exit function is an example of parameter expansion. You can
surround a variable name with curly braces (as with ${PROGNAME}) if you need to be sure it is separated from
surrounding text. Some people just put them around every variable out of habit. That usage is simply a style
thing. The second use,${1:-"Unknown Error"} means that if parameter 1 ($1) is undefined, substitute the string
"Unknown Error" in its place. Using parameter expansion, it is possible to perform a number of useful string
manipulations. You can read more about parameter expansion in the bash man page under the topic
"EXPANSIONS".
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.
• LinuxCommand
• Script library
• SuperMan pages
________________________________________
Errors are not the only way that a script can terminate unexpectedly. You also have to be concerned with
signals. Consider the following program:
#!/bin/bash
echo "this script will endlessly loop until you stop it"
while true; do
: # Do nothing
done
After you launch this script it will appear to hang. Actually, like most programs that appear to hang, it is really
stuck inside a loop. In this case, it is waiting for the true command to return a non-zero exit status, which it
never does. Once started, the script will continue until bash receives a signal that will stop it. You can send such
a signal by typing ctrl-c which is the signal called SIGINT (short for SIGnal INTerrupt).
OK, so a signal can come along and make your script terminate. Why does it matter? Well, in many cases it
doesn't matter and you can ignore signals, but in some cases it will matter.
#!/bin/bash
TEMP_FILE=/tmp/printfile.txt
pr $1 > $TEMP_FILE
read
lpr $TEMP_FILE
fi
This script processes a text file specified on the command line with the prcommand and stores the result in a
temporary file. Next, it asks the user if they want to print the file. If the user types "y", then the temporary file is
passed to the lpr program for printing (you may substitute less for lpr if you don't actually have a printer
attached to your system.)
Now, I admit this script has a lot of design problems. While it needs a file name passed on the command line, it
doesn't check that it got one, and it doesn't check that the file actually exists. But the problem I want to focus on
here is the fact that when the script terminates, it leaves behind the temporary file.
Good practice would dictate that we delete the temporary file $TEMP_FILE when the script terminates. This is
easily accomplished by adding the following to the end of the script:
rm $TEMP_FILE
This would seem to solve the problem, but what happens if the user types ctrl-c when the "Print file? [y/n]:"
prompt appears? The script will terminate at the read command and the rm command is never executed.
Clearly, we need a way to respond to signals such as SIGINT when the ctrl-c key is typed.
Fortunately, bash provides a method to perform commands if and when signals are received.
trap
The trap command allows you to execute a command when a signal is received by your script. It works like this:
"signals" is a list of signals to intercept and "arg" is a command to execute when one of the signals is received.
For our printing script, we might handle the signal problem this way:
#!/bin/bash
TEMP_FILE=/tmp/printfile.txt
trap "rm $TEMP_FILE; exit" SIGHUP SIGINT SIGTERM
pr $1 > $TEMP_FILE
read
lpr $TEMP_FILE
fi
rm $TEMP_FILE
Here we have added a trap command that will execute "rm $TEMP_FILE" if any of the listed signals is received.
The three signals listed are the most common ones that you will encounter, but there are many more that can
be specified. For a complete list, type "trap -l". In addition to listing the signals by name, you may alternately
specify them by number.
There is one signal that you cannot trap: SIGKILL or signal 9. The kernel immediately terminates any process sent
this signal and no signal handling is performed. Since it will always terminate a program that is stuck, hung, or
otherwise screwed up, it is tempting to think that it's the easy way out when you have to get something to stop
and go away. Often you will see references to the following command which sends the SIGKILL signal:
kill -9
However, despite its apparent ease, you must remember that when you send this signal, no processing is done
by the application. Often this is OK, but with many programs it's not. In particular, many complex programs (and
some not-so-complex) create lock files to prevent multiple copies of the program from running at the same
time. When a program that uses a lock file is sent a SIGKILL, it doesn't get the chance to remove the lock file
when it terminates. The presence of the lock file will prevent the program from restarting until the lock file is
manually removed.
A clean_up function
While the trap command has solved the problem, we can see that it has some limitations. Most importantly, it
will only accept a single string containing the command to be performed when the signal is received. You could
get clever and use ";" and put multiple commands in the string to get more complex behavior, but frankly, it's
ugly. A better way would be to create a function that is called when you want to perform any actions at the end
of your script. In my scripts, I call this function clean_up.
#!/bin/bash
TEMP_FILE=/tmp/printfile.txt
function clean_up {
rm $TEMP_FILE
exit
pr $1 > $TEMP_FILE
read
lpr $TEMP_FILE
fi
clean_up
The use of a clean up function is a good idea for your error handling routines too. After all, when your program
terminates (for whatever reason), you should clean up after yourself. Here is finished version of our program
with improved error and signal handling:
#!/bin/bash
if [ -d "~/tmp" ]; then
TEMP_DIR=~/tmp
else
TEMP_DIR=/tmp
fi
TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM
PROGNAME=$(basename $0)
function usage {
function clean_up {
rm -f $TEMP_FILE
exit $1
function error_exit {
clean_up 1
if [ $# != "1" ]; then
usage
fi
if [ ! -f "$1" ]; then
fi
read
fi
clean_up
In the program above, there a number of steps taken to help secure the temporary file used by this script. It is a
Unix tradition to use a directory called /tmp to place temporary files used by programs. Everyone may write files
into this directory. This naturally leads to some security concerns. If possible, avoid writing files in the /tmp
directory. The preferred technique is to write them in a local directory such as ~/tmp (a tmp subdirectory in the
user's home directory.) If you must write files in/tmp, you must take steps to make sure the file names are not
predictable. Predictable file names allow an attacker to create symbolic links to other files that the attacker
wants you to overwrite.
A good file name will help you figure out what wrote the file, but will not be entirely predictable. In the script
above, the following line of code created the temporary file $TEMP_FILE:
TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM
The $TEMP_DIR variable contains either /tmp or ~/tmp depending on the availability of the directory. It is
common practice to embed the name of the program into the file name. We have done that with the string
"printfile". Next, we use the $$ shell variable to embed the process id (pid) of the program. This further helps
identify what process is responsible for the file. Surprisingly, the process id alone is not unpredictable enough to
make the file safe, so we add the $RANDOM shell variable to append a random number to the file name. With
this technique, we create a file name that is both easily identifiable and unpredictable.
________________________________________
© 2000-2013, William E. Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any
medium, provided this copyright notice is preserved.