Linux tips: A Quick Introduction to Bash Programming (Part 2)
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: |
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()…” |
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
hello |
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 “Enter your choice: “
|
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
trap action signal |
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 # count down from 10 to 1:
|
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.
Via codecoffee