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 called branching 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
echo -n "Enter a number between 1 and 3 inclusive > "
read character
if [ "$character" = "1" ]; then
echo "You entered one."
else
if [ "$character" = "2" ]; then
echo "You entered two."
else
if [ "$character" = "3" ]; then
echo "You entered three."
else
echo "You did not enter a number"
echo "between 1 and 3."
fi
fi
fi
Not very pretty.
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
echo -n "Enter a number between 1 and 3 inclusive > "
read character
case $character in
1 ) echo "You entered one."
;;
2 ) echo "You entered two."
;;
3 ) echo "You entered three."
;;
* ) echo "You did not enter a number"
echo "between 1 and 3."
esac
The case command has the following form:
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
echo -n "Type a digit or a letter > "
read character
case $character in
# Check for letters
[a-z] | [A-Z] ) echo "You typed the letter $character"
;;
# Check for digits
[0-9] ) echo "You typed the digit $character"
;;
# Check for anything else
* ) echo "You did not type a letter or a digit"
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.
#!/bin/bash echo -n "Enter a number between 1 and 3 inclusive > " read character if [ "$character" = "1" ]; then echo "You entered one." else if [ "$character" = "2" ]; then echo "You entered two." else if [ "$character" = "3" ]; then echo "You entered three." else echo "You did not enter a number" echo "between 1 and 3." fi fi fi
#!/bin/bash echo -n "Enter a number between 1 and 3 inclusive > " read character case $character in 1 ) echo "You entered one." ;; 2 ) echo "You entered two." ;; 3 ) echo "You entered three." ;; * ) echo "You did not enter a number" echo "between 1 and 3." esac
case word in patterns ) statements ;; esac
#!/bin/bash echo -n "Type a digit or a letter > " read character case $character in # Check for letters [a-z] | [A-Z] ) echo "You typed the letter $character" ;; # Check for digits [0-9] ) echo "You typed the digit $character" ;; # Check for anything else * ) echo "You did not type a letter or a digit" esac
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 until in 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
while [ $number -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
On line 3, we create a variable called number and initialize its value to 0. Next, we start the while loop. 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 while example to achieve the same result:
#!/bin/bash
number=0
until [ $number -ge 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
#!/bin/bash number=0 while [ $number -lt 10 ]; do echo "Number = $number" number=$((number + 1)) done
#!/bin/bash number=0 until [ $number -ge 10 ]; do echo "Number = $number" 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=
until [ "$selection" = "0" ]; do
echo ""
echo "PROGRAM MENU"
echo "1 - display free disk space"
echo "2 - display free memory"
echo ""
echo "0 - exit program"
echo ""
echo -n "Enter selection: "
read selection
echo ""
case $selection in
1 ) df ;;
2 ) free ;;
0 ) exit ;;
* ) echo "Please enter 1, 2, or 0"
esac
done
The purpose of the until loop 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 ""
echo -n "Press Enter to continue"
read
clear
}
selection=
until [ "$selection" = "0" ]; do
echo ""
echo "PROGRAM MENU"
echo "1 - display free disk space"
echo "2 - display free memory"
echo ""
echo "0 - exit program"
echo ""
echo -n "Enter selection: "
read selection
echo ""
case $selection in
1 ) df ; press_enter ;;
2 ) free ; press_enter ;;
0 ) exit ;;
* ) echo "Please enter 1, 2, or 0"; press_enter
esac
done
#!/bin/bash selection= until [ "$selection" = "0" ]; do echo "" echo "PROGRAM MENU" echo "1 - display free disk space" echo "2 - display free memory" echo "" echo "0 - exit program" echo "" echo -n "Enter selection: " read selection echo "" case $selection in 1 ) df ;; 2 ) free ;; 0 ) exit ;; * ) echo "Please enter 1, 2, or 0" esac done
#!/bin/bash function press_enter { echo "" echo -n "Press Enter to continue" read clear } selection= until [ "$selection" = "0" ]; do echo "" echo "PROGRAM MENU" echo "1 - display free disk space" echo "2 - display free memory" echo "" echo "0 - exit program" echo "" echo -n "Enter selection: " read selection echo "" case $selection in 1 ) df ; press_enter ;; 2 ) free ; press_enter ;; 0 ) exit ;; * ) echo "Please enter 1, 2, or 0"; press_enter esac done
When your computer hangs...
Positional Parameters
When we last left our script, it looked something like this:
#!/bin/bash # system_page - A script to produce a system information HTML file ##### Constants TITLE="System Information for $HOSTNAME" RIGHT_NOW=$(date +"%x %r %Z") TIME_STAMP="Updated on $RIGHT_NOW by $USER" ##### Functions function system_info { echo "<h2>System release info</h2>" echo "<p>Function not yet implemented</p>" } # end of system_info function show_uptime { echo "<h2>System uptime</h2>" echo "<pre>" uptime echo "</pre>" } # end of show_uptime function drive_space { echo "<h2>Filesystem space</h2>" echo "<pre>" df echo "</pre>" } # end of drive_space function home_space { # Only the superuser can get this information if [ "$(id -u)" = "0" ]; then echo "<h2>Home directory space by user</h2>" echo "<pre>" echo "Bytes Directory" du -s /home/* | sort -nr echo "</pre>" fi } # end of home_space ##### Main cat <<- _EOF_ <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:
- 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.
- 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.
- 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.
Let's imagine the following command line:
[me@linuxbox me]$ some_program word1 word2 word3
If some_program were a bash shell script, we could read each item on the command line because the positional parameters contain the following:
- $0 would contain "some_program"
- $1 would contain "word1"
- $2 would contain "word2"
- $3 would contain "word3"
Here is a script you can use to try this out:
#!/bin/bash echo "Positional Parameters" echo '$0 = ' $0 echo '$1 = ' $1 echo '$2 = ' $2 echo '$3 = ' $3
Detecting command line arguments
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 if [ "$1" != "" ]; then echo "Positional parameter 1 contains something" else echo "Positional parameter 1 is empty" 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 echo "Your command line contains $# arguments" else echo "Your command line contains no arguments" fi
Command line options
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.
Here is the code we will use to process our command line:
interactive= filename=~/system_page.html while [ "$1" != "" ]; do case $1 in -f | --file ) shift filename=$1 ;; -i | --interactive ) interactive=1 ;; -h | --help ) usage exit ;; * ) usage exit 1 esac shift done
This code is a little tricky, so bear with me as I attempt to explain it.
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 variable filename 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 while loop 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, $3 becomes $2, $4 becomes $3, and so on. Try this:
#!/bin/bash echo "You start with $# positional parameters" # Loop until all parameters are used up while [ "$1" != "" ]; do echo "Parameter 1 equals $1" echo "You now have $# positional parameters" # Shift all the parameters down by one shift done
Getting an option's argument
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 to filename. Later we will have to check the content of filename to make sure it is valid.
Integrating the command line processor into the script
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 # system_page - A script to produce a system information HTML file ##### Constants TITLE="System Information for $HOSTNAME" RIGHT_NOW=$(date +"%x %r %Z") TIME_STAMP="Updated on $RIGHT_NOW by $USER" ##### Functions function system_info { echo "<h2>System release info</h2>" echo "<p>Function not yet implemented</p>" } # end of system_info function show_uptime { echo "<h2>System uptime</h2>" echo "<pre>" uptime echo "</pre>" } # end of show_uptime function drive_space { echo "<h2>Filesystem space</h2>" echo "<pre>" df echo "</pre>" } # end of drive_space function home_space { # Only the superuser can get this information if [ "$(id -u)" = "0" ]; then echo "<h2>Home directory space by user</h2>" echo "<pre>" echo "Bytes Directory" du -s /home/* | sort -nr echo "</pre>" fi } # end of home_space function write_page { cat <<- _EOF_ <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 { echo "usage: system_page [[[-f file ] [-i]] | [-h]]" } ##### Main interactive= filename=~/system_page.html while [ "$1" != "" ]; do case $1 in -f | --file ) shift filename=$1 ;; -i | --interactive ) interactive=1 ;; -h | --help ) usage exit ;; * ) usage exit 1 esac shift done # Test code to verify command line processing if [ "$interactive" = "1" ]; then echo "interactive is on" else echo "interactive is off" fi echo "output file = $filename" # Write page (comment out until testing is complete) # write_page > $filename
Adding interactive mode
The interactive mode is implemented with the following code:
if [ "$interactive" = "1" ]; then response= echo -n "Enter name of output file [$filename] > " read response if [ -n "$response" ]; then filename=$response fi if [ -f $filename ]; then echo -n "Output file exists. Overwrite? (y/n) > " read response if [ "$response" != "y" ]; then echo "Exiting program." 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:
echo -n "Enter name of output file [$filename] > "
We display the current value of filename since, the way this routine is coded, if the user just presses the enter key, the default value of filename will be used. This is accomplished in the next two lines where the value of response is checked. If response is not empty, then filename is assigned the value of response. Otherwise, filename is left unchanged, preserving its default value.
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.
No comments:
Post a Comment