In this second article, Harold continues
with his fast paced, excellent introduction to Bash Programming.
This time he explains how to perform arithmetic operations in
your bash scripts. He also explains how to define functions
in your programs. Finally he concludes with an introduction
to advanced concepts such as reading user inputs in your bash
scripts, accepting arguments to the scripts, trapping signals
and also understanding return values of programs.
This is definitely much more than you must have expected...
Once you read this you would no longer be a beginner.. you would
already be on your way to master Bash programming!!
Arithmetic with Bash
bash allows you to perform arithmetic
expressions. As you have already seen, arithmetic is performed
using the expr command. However, this, like the true command,
is considered to be slow. The reason is that in order to run
true and expr, the shell has to start them up. A better way
is to use a built in shell feature which is quicker. So an alternative
to true, as we have also seen, is the ":" command.
An alternative to using expr, is to enclose the arithmetic operation
inside $((...)). This is different from $(...). The number of
brackets will tell you that. Let us try it
|
#!/bin/bash
x=8 # initialize x to 8
y=4 # initialize y to 4
# now we assign the sum of x and y to z:
z=$(($x + $y))
echo "The sum of $x + $y is $z"
|
As always, whichever one you choose,
is purely up to you. If you feel more comfortable using expr
to $((...)), by all means, use it.
bash is able to perform, addition,
subtraction, multiplication, division, and modulus. Each action
has an operator that corresponds to it
|
ACTION
|
OPERATOR
|
|
Addition
|
+
|
|
Subtraction
|
-
|
|
Multiplication
|
*
|
|
Division
|
/
|
|
Modulus
|
%
|
Everyone should be familiar with the
first four operations. If you do not know what modulus is, it
is the value of the remainder when two values are divided. Here
is an example of arithmetic in bash
|
#!/bin/bash
x=5 # initialize x to 5
y=3 # initialize y to 3
add=$(($x + $y)) # add the values of x and y and
assign it to variable add
sub=$(($x - $y)) # subtract the values of x and
y and assign it to variable sub
mul=$(($x * $y)) # multiply the values of x and
y and assign it to variable mul
div=$(($x / $y)) # divide the values of x and y
and assign it to variable div
mod=$(($x % $y)) # get the remainder of x / y and
assign it to variable mod
# print out the answers:
echo "Sum: $add"
echo "Difference: $sub"
echo "Product: $mul"
echo "Quotient: $div"
echo "Remainder: $mod"
|
Again, the above code could have been
done with expr. For instance, instead of add=$(($x + $y)), you
could have used add=$(expr $x + $y), or, add=`expr $x + $y`.
Reading User Input
Now we come to the fun part. You can
make your program so that it will interact with the user, and
the user can interact with it. The command to get input from
the user, is read. read is a built in bash command that needs
to make use of variables, as you will see
|
#!/bin/bash
# gets the name of the user and prints a greeting
echo -n "Enter your name: "
read user_name
echo "Hello $user_name!"
|
The variable here is user_name. Of
course you could have called it anything you like. read will
wait for the user to enter something and then press ENTER. If
the user does not enter anything, read will continue to wait
until the ENTER key is pressed. If ENTER is pressed without
entering anything, read will execute the next line of code.
Try it. Here is the same example, only this time we check to
see if the user enters something
|
#!/bin/bash
# gets the name of the user and prints a greeting
echo -n "Enter your name: "
read user_name
# the user did not enter anything:
if [ -z "$user_name" ]; then
echo "You did not tell me your name!"
exit
fi
echo "Hello $user_name!"
|
Here, if the user presses the ENTER
key without typing anything, our program will complain and exit.
Otherwise, it will print the greeting. Getting user input is
useful for interactive programs that require the user to enter
certain things.
Functions
Functions make scripts easier to maintain.
Basically it breaks up the program into smaller pieces. A function
performs an action defined by you, and it can return a value
if you wish. Before I continue, here is an example of a shell
program using a function
|
#!/bin/bash
# function hello() just prints a message
hello()
{
echo "You are in function hello()"
}
echo "Calling function hello()..."
# call the hello() function:
hello
echo "You are now out of function hello()"
|
Try running the above. The function
hello() has only one purpose here, and that is, to print a message.
Functions can of course be made to do more complicated tasks.
In the above, we called the hello() function by name by using
the line
When this line is executed, bash searches
the script for the line hello(). It finds it right at the top,
and executes its contents.
Functions are always called by their
function name, as we have seen in the above. When writing a
function, you can either start with function_name(), as we did
in the above, or if you want to make it more explicit, you can
use the function function_name(). Here is an alternative way
to write function hello()
|
function hello()
{
echo "You are in function hello()"
}
|
Functions always have an empty start
and closing brackets: "()", followed by a starting
brace and an ending brace: "{...}". These braces mark
the start and end of the function. Any code enclosed within
the braces will be executed and will belong only to the function.
Functions should always be defined before they are called. Let
us look at the above program again, only this time we call the
function before it is defined
|
#!/bin/bash
echo "Calling function hello()..."
# call the hello() function:
hello
echo "You are now out of function hello()" #
function hello() just prints a message
hello()
{
echo "You are in function hello()"
}
|
Here is what we get when we try to
run it
$ ./hello.sh
Calling function hello()...
./hello.sh: hello: command not found
You are now out of function hello()
As you can see, we get an error. Therefore, always have your
functions at the start of your code, or at least, before you
call the function. Here is another example of using functions
|
#!/bin/bash
# admin.sh - administrative tool
# function new_user() creates a new user account
new_user()
{
echo "Preparing to add a new user..."
sleep 2
adduser # run the adduser
program
}
echo "1. Add user"
echo "2. Exit"
echo "Enter your choice:
"
read choice
case $choice in
1) new_user # call the new_user()
function
;;
*) exit
;;
esac
|
In order for this to work properly,
you will need to be the root user, since adduser is a program
only root can run. Hopefully this example (short as it is) shows
the usefulness of functions.
Trapping
You can use the built in command trap
to trap signals in your programs. It is a good way to gracefully
exit a program. For instance, if you have a program running,
hitting CTRL-C will send the program an interrupt signal, which
will kill the program. trap will allow you to capture this signal,
and will give you a chance to either continue with the program,
or to tell the user that the program is quitting. trap uses
the following syntax
action is what you want to do when
the signal is activated, and signal is the signal to look for.
A list of signals can be found by using the command trap -l.
When using signals in your shell programs, omit the first three
letters of the signal, usually SIG. For instance, the interrupt
signal is SIGINT. In your shell programs, just use INT. You
can also use the signal number that comes beside the signal
name. For instance, the numerical signal value of SIGINTis 2.
Try out the following program
|
#!/bin/bash
# using the trap command
# trap CTRL-C and execute the sorry() function:
trap sorry INT
# function sorry() prints a message
sorry()
{
echo "I'm sorry Dave. I can't do that."
sleep 3
}
# count down from 10 to 1:
for i in 10 9 8 7 6 5 4 3 2 1; do
echo $i seconds until system failure."
sleep 1
done
echo "System failure."
|
Now, while the program is running
and counting down, hit CTRL-C. This will send an interrupt signal
to the program. However, the signal will be caught by the trap
command, which will in turn execute the sorry() function. You
can have trap ignore the signal by having "''" in
place of the action. You can reset the trap by using a dash:
"-". For instance
|
# execute the sorry() function
if SIGINT is caught:
trap sorry INT
# reset the trap:
trap - INT
# do nothing when SIGINT is caught:
trap '' INT
|
When you reset a trap, it defaults
to its original action, which is, to interrupt the program and
kill it. When you set it to do nothing, it does just that. Nothing.
The program will continue to run, ignoring the signal.
Boolean AND & OR
We have seen the use of control structures,
and how useful they are. There are two extra things that can
be added. The AND: "&&" and the OR "||"
statements. The AND statement looks like this
| condition_1 && condition_2 |
The AND statement first checks the
leftmost condition. If it is true, then it checks the second
condition. If it is true, then the rest of the code is executed.
If condition_1 returns false, then condition_2 will not be executed.
In other words
| if condition_1 is true, AND if
condition_2 is true, then... |
Here is an example making use of the
AND statement
|
#!/bin/bash
x=5
y=10
if [ "$x" -eq 5 ] && [ "$y"
-eq 10 ]; then
echo "Both conditions are true."
else
echo "The conditions are not true."
fi
|
Here, we find that x and y both hold
the values we are checking for, and so the conditions are true.
If you were to change the value of x=5 to x=12, and then re-run
the program, you would find that the condition is now false.
The OR statement is used in a similar
way. The only difference is that it checks if the leftmost statement
is false. If it is, then it goes on to the next statement, and
the next
| condition_1 || condition_2 |
In pseudo code, this would translate
to the following
| if condition_1 is true, OR if
condition_2 is true, then... |
Therefore, any subsequent code will
be executed, provided at least one of the tested conditions
is true
|
#!/bin/bash
x=3
y=2
if [ "$x" -eq 5 ] || [ "$y" -eq 2
]; then
echo "One of the conditions is true."
else
echo "None of the conditions are true."
fi
|
Here, you will see that one of the
conditions is true. However, change the value of y and re-run
the program. You will see that none of the conditions are true.
If you think about it, the if structure
can be used in place of AND and OR, however, it would require
nesting the if statements. Nesting means having an if structure
within another if structure. Nesting is also possible with other
control structures of course. Here is an example of a nested
if structure, which is an equivalent of our previous AND code
|
#!/bin/bash
x=5
y=10
if [ "$x" -eq 5 ]; then
if [ "$y" -eq 10 ]; then
echo "Both conditions
are true."
else
echo "The conditions
are not true."
fi
fi
|
This achieves the same purpose as using
the AND statement. It is much harder to read, and takes much
longer to write. Save yourself the trouble and use the AND and
OR statements.
Using Arguments
You may have noticed that most programs
in Linux are not interactive. You are required to type arguments,
otherwise, you get a "usage" message. Take the more
command for instance. If you do not type a filename after it,
it will respond with a "usage" message. It is possible
to have your shell program work on arguments. For this, you
need to know the "$#" variable. This variable stands
for the total number of arguments passed to the program. For
instance, if you run a program as follows:
$ foo argument
$# would have a value of one, because
there is only one argument passed to the program. If you have
two arguments, then $# would have a value of two. In addition
to this, each word on the command line, that is, the program's
name (in this case foo), and the argument, can be referred to
as variables within the shell program. foo would be $0. argument
would be $1. You can have up to 9 variables, from $0 (which
is the program name), followed by $1 to $9 for each argument.
Let us see this in action
|
#!/bin/bash
# prints out the first argument
# first check if there is an argument:
if [ "$#" -ne 1 ]; then
echo "usage: $0 <argument>"
fi echo "The argument is $1"
|
This program expects one, and only
one, argument in order to run the program. If you type less
than one argument, or more than one, the program will print
the usage message. Otherwise, if there is an argument passed
to the program, the shell program will print out the argument
you passed. Recall that $0 is the program's name. This is why
it is used in the "usage" message. The last line makes
use of $1. Recall that $1 holds the value of the argument that
is passed to the program.
Temporary Files
Often, there will be times when you
need to create a temporary file. This file may be to temporarily
hold some data, or just to work with a program. Once the program's
purpose is completed, the file is often deleted. When you create
a file, you have to give it a name. The problem is, the file
you create, must not already existing in the directory you are
creating it in. Otherwise, you could overwrite important data.
In order to create a unique named temporary file, you need to
use the "$$" symbol, by either prefixing, or suffixing
it to the end of the file name. Take for example, you want to
create a temporary file with the name hello. Now there is a
chance that the user who runs your program, may have a file
called hello, so that would clash with your program's temporary
file. By creating a file called hello.$$, or $$hello instead,
you will create a unique file. Try it
$ touch hello
$ ls
hello
$ touch hello.$$
$ ls
hello hello.689
There it is, your temporary file.
Return Values
Most programs return a value depending
upon how they exit. For instance, if you look at the manual
page for grep, it tells us that grep will return a 0 if a match
was found, and a 1 if no match was found. Why do we care about
the return value of a program? For various reasons. Let us say
that you want to check if a particular user exists on the system.
One way to do this would be to grep the user's name in the /etc/passwd
file. Let us say the user's name is foobar
$ grep "foobar" /etc/passwd
$
No output. That means that grep could
not find a match. But it would be so much more helpful if a
message saying that it could not find a match was printed. This
is when you will need to capture the return value of the program.
A special variable holds the return value of a program. This
variable is $?. Take a look at the following piece of code
|
#!/bin/bash
# grep for user foobar and pipe all output to /dev/null:
grep "foobar" /etc/passwd > /dev/null 2>&1
# capture the return value and act accordingly:
if [ "$?" -eq 0 ]; then
echo "Match found."
exit
else
echo "No match found."
fi
|
Now when you run the program, it will
capture the return value of grep. If it equals to 0, then a
match was found and the appropriate message is printed. Otherwise,
it will print that there was no match found. This is a very
basic use of getting a return value from a program. As you continue
practicing, you will find that there will be times when you
need the return value of a program to do what you want.
If you happen to be wondering what
2>&1 means, it is quite simple. Under Linux, these numbers
are file descriptors. 0 is standard input (eg: keyboard), 1
is standard output (eg: monitor) and 2 is standard error (eg:
monitor). All normal information is sent to file descriptor
1, and any errors are sent to 2. If you do not want to have
the error messages pop up, then you simply redirect it to /dev/null.
Note that this will not stop information from being sent to
standard output. For example, if you do not have permissions
to read another user's directory, you will not be able to list
its contents
$ ls /root
ls: /root: Permission denied
$ ls /root 2> /dev/null
$
As you can see, the error was not printed
out this time. The same applies for other programs and for file
descriptor 1. If you do not want to see the normal output of
a program, that is, you want it to run silently, you can redirect
it to /dev/null. Now if you do not want to see either standard
input or error, then you do it this way
$ ls /root > /dev/null 2>&1
This means that the program will send
any output or errors that occur to /dev/null so you never ever
see them.
Now what if you want your shell script
to return a value upon exiting? The exit command takes one argument.
A number to return. Normally the number 0 is used to denote
a successful exit, no errors occurred. Anything higher or lower
than 0 normally means an error has occurred. This is for you,
the programmer to decide. Let us look at this program
|
#!/bin/bash
if [ -f "/etc/passwd" ]; then
echo "Password file exists."
exit 0
else
echo "No such file."
exit 1
fi
|
By specifying return values upon exit,
other shell scripts you write making use of this script will
be able to capture its return value.
Porting your Bash Scripts
It is important to try and write your
scripts so that they are portable. This means that if your script
works under Linux, then it should work in another Unix system
with as little modification as possible, if any. In order to
do this, you should be careful when calling external programs.
First consider the question, "Is this program going to
be available in this other Unix variant?". Recall that
if you have a program foo that works in the same way as echo,
you can use it in echo's place. However, if it so happens that
your script is used on a Unix system without the foo program,
then your script will start reporting errors. Also, keep in
mind that different versions of bash may have new ways to do
different things. For instance, the VAR=$(ps) does the same
thing as VAR=`ps`, but realize that older shells, for instance
the Bourne shell, only recognizes the latter syntax. Be sure
that if you are going to distribute your scripts, you include
a README text file which warns the user of any surprises, including,
the version of bash the script was tested on, as well as any
required programs or libraries needed by the script to run.
Conclusion
That completes the introduction to
bash scripting. Your scripting studies are not complete however.
There is more to cover. As I said, this is an introduction.
However, this is enough to get you started on modifying shell
programs and writing your own. If you really want to master
shell scripting, I recommend buying Learning the bash shell,
2nd Edition by O'Reilly & Associates, Inc. bash scripting
is great for your everyday administrative use. But if you are
planning on a much bigger project, you will want to use a much
more powerful language like C or Perl.
Good luck...
About the Author - Harold Rodriguez
( xconsole [at] it.yorku.ca ) maintains a site called Moonfrog.
It has many useful tutorials on various topics. You could
access his site at http://it.yorku.ca/moonfrog
|
Note
: Please replace the [at] with a @ in the email
id of the author.
|
|

|
©
Copyright by Harold Rodriguez. All rights reserved. Contact
the author for permissions.
|