Scripting

by Chris Herborth

Section 1 What Is Scripting
Section 2 Application Scripting Languages for BeOS
Section 3 Setting Up a Script
Section 4 Shell Scripting 101
Section 5 Really Advanced Shell Use
Section 6 BeOS Application Scripting
Section 7 Making Your Scripts Run from the Tracker
In this section:

Really Advanced Shell Use

   Script Arguments
      Positional Parameters
      "shift" Work
      The Whole Shebang
      Counting Parameters
      A Bit of 'Rithmetic

   More Looping
      The while Loop
      The until Loop
      Skipping Out and Breaking Things
      The break Statement

   Case Study

   Listening to the User
      The read Command
      Making Alerts

   Using Functions
      Functions in Scripts
      Functions in the Shell

   Debugging Your Scripts


Really Advanced Shell Use

Now that we've seen everything we've talked about (for loops, if statements, and substitutions) in use, we should look at a few shell script techniques that can make your life easier.

Script Arguments

As you've seen, shell scripts can have command-line arguments just like normal programs: options to control the behavior of the script, the names of files and directories to operate on, or both.

You've also seen that you can access these arguments from a special variable--$@--and loop through them using a for loop. There's a lot more you can do with a script's command-line arguments, though!

Positional Parameters A script can access the first ten arguments (starting with the script's name and ending with the ninth argument) using $0, $1,...$9. These are called "positional parameters" by shell freaks because they're referred to by their position in the command line. You can test this by creating a simple script like this:

#! /bin/sh
echo 0 is: $0
echo 1 is: $1
echo 2 is: $2

and running it with various fictional parameters. For example, if you named this script EchoTest, you could type

$ EchoTest hello zoomer

and the shell would spit back:

0 is: EchoTest
1 is: hello
2 is: zoomer

Try it a few times with different numbers of parameters. If there are fewer than three arguments, you won't get an error--you'll just get a line like "2 is:" with nothing else there.

The $0, $1,...$9 variables are created for you by the shell, just like the $@ variable. Why are we limited to ten of these? It's another artifact of the way the ancient shell handles commands, and it was probably designed this way because there are only ten digits on an English keyboard.

If you miss the ren command from DOS (used to rename files), you could write yourself a simple shell script to mimic it:

#! /bin/sh
#
# Act like the DOS "ren" command.

# If the first argument or the second argument isn't there,
# print an error message and exit.

if [ "$1" = "" ] || [ "$2" = "" ]; then
echo "usage: ren original new"
exit
fi

# Now rename the first argument to the second argument.
mv $1 $2

As you can see, I keep sneaking in new bits. There are two tests in that if statement, and the || between them means OR. The "usage" message will be displayed if the first argument is blank or if the second argument is blank.

Combining Tests

Just as you can use || to combine two or more tests in an OR sequence (the entire statement will be true if any of the tests are true), you can use && for an AND sequence.

The AND sequence will only be true if all of the tests are true. For example, we can combine the -e test (does a file exist?) with the -r test (can I read this file?) to see if a particular file exists AND we can read it:

#! /bin/sh
if [ -e /path/to/the/file ] && [ -r /path/to/the/file ] ; then
echo "The file exists and we can read it."
else
echo "You're out of luck."
fi

To keep track of OR and AND, just say them: "If the file exists AND we can read it...."

Remember how programs have an exit status to indicate success or failure? You can use the exit statement to get out of a running shell script and return an exit status.

exit by itself will send back an exit status of 0, meaning everything was fine. Want to let the world know you had problems? Send back something else:

exit 1

It's a good idea to return a different exit status for every different kind of problem your script could have. Then when you document these different exit status values, other people can incorporate your script into their shell scripts!

"shift" Work What if you've got more than nine parameters, though? Using $10 won't get you the next one (the shell will use the contents of the first argument in the $1 variable with a "0" appended to the end), but there is a way around this. The shift command will shift all the arguments in $2 ...$9 down by one "slot," and assign the next argument to $9. Try adding this to the end of the simple script we just created to play with arguments:

shift
echo 0 is: $0
echo 1 is: $1
echo 2 is: $2

Try running it again with different numbers of arguments. I've named mine foo and called it with:

$ foo a b c

and the shell prints:

0 is: ./foo
1 is: a
2 is: b
0 is: ./foo
1 is: b
2 is: c

The first parameter ("a") has fallen off the front of the list of arguments and the other two parameters have moved down, which lets us see the last argument.

This may seem pretty pointless and complex, but it's actually useful. Imagine that you've got a shell script that takes a few options to control its behavior, and that it also takes a bunch of filenames as arguments:

$ myscript -v -stress ugly.txt nicer.html *.jpg

For this example, we'll assume that if you use -v it must come first, then -stress, then the arguments. If you run through all of the arguments with a for loop, you'll have to check every argument inside the loop to handle the -v and -stress options:

!# /bin/sh

# Before we start the loop, we should set up some defaults.
# We'll use an empty variable to mean "this is off"; if the user
# turns them on, we'll set them to "yes".

verbose=
stressed=

# Run through the arguments...

for arg in "$@" ; do

# Check for the -v option.

if [ "$arg" = "-v" ] ; then
verbose="yes"

# Now we've handled this, let's go on to the next.

continue
fi

# Check for the -stress option.

if [ "$arg" = "-stress" ] ; then
stressed="yes"

# Now we've handled this, let's go on to the next.

continue
fi

# Do your work on anything that isn't an option.

...
done

This makes the loop through the arguments more complex, and you're testing for the -v and -stress options every time, whether you've seen them already or not.

By using the shift statement, we can do this outside the for loop, and still run through the arguments using the $@ variable:

#! /bin/sh

# Before we start the loop, check for the -v and -stress options.

# If the first argument is -v, we'll set the verbose variable to yes.
# Then the shift statement will kick -v out of the list of arguments,
# and move the rest of them down one "slot".

if [ "$1" = "-v" ] ; then
verbose="yes"
shift
fi

# Do the same for "-stress". If -v was the first argument before,
# -stress will be the first after the shift statement.

if [ "$1" = "-stress" ] ; then
stress="yes"
shift
fi

# Now we can loop through the rest of the arguments; the -v and -stress
# options will have been removed from the list of arguments by the
# shift statements.

for arg in "$@" ; do

# Do your work on anything that isn't an option.

...
done

In a "real" shell script with complex arguments like this, you'd have two loops. The first one would deal with all of the options and use shift to remove them from the list of arguments. The second loop would then run through the remaining arguments to do the work.

The Whole Shebang We've already used $@ to go through a script's arguments, but you can also access a script's arguments with $*. When $@ and $* appear by themselves, they behave the same, but if you put them in quotes ("$@" and "$*"), you get different results.

When the shell sees "$@" it runs through the parameters as if each one were a separate item; with "$*" it treats all of the parameters as one line separated by spaces. To test this, put the following in a script and try it out:

#! /bin/sh
echo "First with @..."
for i in "$@" ; do
echo $i
done

echo "Now with *..."

for i in "$*" ; do
echo $i
done

The first loop will print the arguments one at a time on separate lines. The second version will print all of the arguments together on one line. This could be useful if you wanted to pass the arguments on to another script or program, but it could also cause problems if you were working with files and directories that had spaces in their names. Mixing the shell with files that have spaces in their names is tricky and best avoided if possible.

There is a safe, reliable way of dealing with files and directories that have spaces in their names, but to show it to you I've got to use two things you haven't seen before. Don't worry, we'll talk about them soon; I wanted to let you know about this since I brought up filenames with spaces.

Find a directory that has some filenames with spaces in them and type this into the Terminal:

$ ls -1 | while read arg ; do
> echo "The file is named: $arg"
> done

You'll get back a list of the files:

The file is named: a long filename with spaces in it
The file is named: another crazy filename

The while loop runs as long as something is "true"; in this case, as long as the read statement can read a line of text and assign it to the variable named arg. What text? Well, the list of files that the ls -1 command is printing (that argument is a 1); the -1 option says "give me the list of files, one per line." Of course, you could get a list from a file, or the find command, or another script, etc.

The while loop is covered below in the More Looping section, and we'll talk about read in Listening to the User.

Counting Parameters Sometimes it's handy to know how many arguments you've got, and you can find out using $#. This would let us simplify our DOS-like ren command:

#! /bin/sh

# Make sure we've got at least two arguments; if the number of
# arguments is less than two, print the error message and exit.

if [ $# -lt 2 ]; then
echo "usage: ren original new"
exit
fi

mv $1 $2

If you're going to test $#, place it before the shift command in your scripts! Arguments dropped with the shift command are gone for good, and $# will go down by one (be "decremented" in geekspeak) every time you use shift.

A Bit of 'Rithmetic Doing math from the shell can be hard (you're better off using expr, which was discussed in Chapter 6), but sometimes it can be handy. For "real" math, embedding a call to expr using $(expr ) will be much easier, but if you're doing something very simple like adding or subtracting, the let command is going to be faster because it's built right into the shell.

Let's say you want to look through your command-line arguments and count them. Try putting this in a script and running it with different arguments:

#! /bin/sh
count=1

for i in "$@" ; do
echo argument $count = $i
let count=count+1
done

You'll get a nice numbered list of the arguments. For example, say you named this script testing and ran it with three arguments. You'd see this:

$ testing one two three
argument 1 = one
argument 2 = two
argument 3 = three

Now change the let line to:

count=$(expr $count + 1)

and run it again. Notice how much slower it is? Despite the speed difference, you should use whichever method you find easier. Speed isn't usually a big deal when you're writing a script. Yyou want it to do something for you, and as long as it gets done, who cares if it takes a few seconds...if you wanted speed, you'd learn to program in C or C++!.

The let statement is pretty picky about its syntax; note the total lack of white space. Also note that you don't need a $ to use let on the value of the count variable. Being consistent might be another good reason to use $(expr ...) instead of let, although let is pretty safe if you don't try anything too tricky and stick to the usual math operators of +, -, *, and /.

Note that the let statement only works on integers; if you try to make it work with floating-point numbers, it'll round things off:

$ x=1.1
$ echo $x
1.1
$ let x=x+1
$ echo $x
2

More Looping

If you've ever taken a computer science class, you'll know that sometimes for loops aren't your best bet. For instance, what if you want to do something until a certain condition is true, without looping through a list of arguments?

The while Loop Using the while loop, you can do just that. For example, if I wanted to wait around for a certain file to appear in /tmp (because some other application was running and would eventually create this file that I need for something else), I could use while like this:

#! /bin/sh

# Loop until /tmp/important_document exists.
while [ ! -e /tmp/important_document ] ; then
# Do nothing for 60 seconds.
sleep 60
done

This will go through the loop as long as the test is true; in this case, until a file named /tmp/important_document exists. Each pass through the loop, it sleeps for 60 seconds, giving other processes time to run. Then it goes back, checks for the file again, and so on. Programmers look down thier noses at this and call it "busy waiting" or "polling"; it's the equivalent of someone coming into your office every 60 seconds and saying, "Have you got that important document done yet?" Because we're sleeping inside the loop, however, this isn't as annoying for BeOS as it would be for you.

The form of a while loop is

while TESTS ; do
COMMANDS
done

The COMMANDS will be run over and over until the TESTS are no longer "true" (in the same sense as the if statement we talked about earlier). The TESTS can be anything you'd use with an if statement, such as a command or one of the file, string, or arithmetic tests we talked about earlier.

We could use this and the string substitution from the Substitutions section to take a word and print its letters one to a line. For example, for the input "hello", we'd write:

h
e
l
l
o

This might seem like a weird thing to put in a script, but you never know when you'll find yourself in a weird situation. Something like this should do the trick:

#! /bin/sh

# Go through all of the arguments...

for word in "$@" ; do

# The x variable will be our offset into the word; we set it
# to 0 because that's how programmers spell "first" and we
# want to start with the first letter of the word.

x=0

# Do this loop while x is less than the number of
# characters in this word; remember, the # substitution
# returns the number of characters in the given variable.

while [ $x -lt ${#word} ] ; do

# Now we use this substitution to print 1 character
# from the current word, at the current offset.
#
# As we learned in the Substitution section,
# ${variable:offset:length} will give you "length"
# characters from the contents of "variable", starting
# at "offset".

echo ${word:$x:1}

# Increase the offset by one to move on to the
# next character.

let x=x+1
done
done

For every word in the arguments, this script will set x to 0, then enter the while loop (because 0 will be less than the length of the current word, which we get with the ${#word} substitution). Each letter is printed by extracting one character from the word using x as an offset in the echo command. We increase the value of x by one and head back around for another pass until we've printed all the characters.

Run the script with a few words together as arguments, then change the $@ to $* and try the script again with the same words. Notice the difference? You won't see anything if you're only using one word, but with more than one argument you'll now see a blank line between the words. That's because $* treats the entire command line as one unit, and $@ treats it as separate words. You'll get a blank line for every space on the command line.

The until Loop The until loop looks almost the same as a while loop:

until TESTS ; do
COMMANDS
done

In fact, while and until are exact opposites of each other. With while you're testing something that starts out true and becomes false later, but with until you're testing something that starts out false and becomes true later. You'll stay in the loop executing the COMMANDS until the TESTS are true. Again, as with if and while, the TESTS can be any command or the file, string, and arithmetic tests from the Doing Tests section.

Mnemonic Device If you have trouble remembering which loop is which, just say the commands in plain English: "while this is true, do something" or "until this is true, do something."

We could rewrite our letter printer using until like this:

#! /bin/sh

# Go through all of the arguments...

for word in "$*" ; do

# The x variable will be our offset into the word; we set it
# to 0 because that's how programmers spell "first" and we
# want to start with the first letter of the word.

x=0

# Do this loop until x is greater than or equal to the
# number of characters in this word; remember, the #
# substitution returns the number of characters in the
# given variable.

until [ $x -ge ${#word} ] ; do

# Now we use this substitution to print 1 character
# from the current word, at the current offset.
#
# As we learned in the Substitution section,
# ${variable:offset:length} will give you "length"
# characters from the contents of "variable", starting
# at "offset".

echo ${word:$x:1}

# Increase the offset by one to move on to the
# next character.

let x=x+1
done
done

Instead of printing letters while x is less than the length of the word, we're going to print letters until x is greater than or equal to the length of the word. (Greater than or equal to is the opposite of less than.)

If you're wondering why you have to use a strange constructs like -ge for greater than or equal to rather than the usual >= you learned in high school math, remember that the symbols > and = have special meanings to the shell. These constructs actually make things easier, since you don't have to worry about "escaping" them.

Skipping Out and Breaking Things Sometimes when you're looping through something, you'd like to skip a trip through the loop, or stop looping altogether. For example, what if we didn't like the letter "e" for some reason, and we wanted our letter printer to skip over any "e" that it found? From the section on tests, we know how to find the "e", but how do we skip it?

We do it by using the continue statement, which we've already used in a couple of examples. When the shell's in a for, while, or until loop and sees continue, it skips back to the start of the loop and carries on with the next trip through.

We can change the letter printer as follows:

#! /bin/sh

# Go through all of the arguments...

for word in "$*" ; do

# The x variable will be our offset into the word; we set it
# to 0 because that's how programmers spell "first" and we
# want to start with the first letter of the word.

x=0

# Do this loop until x is greater than or equal to the
# number of characters in this word; remember, the #
# substitution returns the number of characters in the
# given variable.

until [ $x -ge ${#word} ] ; do

# Save the current letter in a handy varible.

letter=${word:$x:1}

# Check to see if it's the evil "e".

if [ "$letter" = "e" ] ; then

# We've found the offending letter; we
# increase x to go on to the next letter,
# then continue with the next trip through
# the "until" loop.

let x=x+1
continue
fi

# If we made it past the if statement, we still like
# this letter. Print the letter, then increase x
# to go on with the next letter.

echo $letter
let x=x+1
done
done

Now whenever the current letter is an "e" the shell heads back to the until loop to get the next letter and carry on. We had to repeat the let statement; otherwise we'd be stuck in what programmers call an "infinite loop." If x stayed the same, the current letter would still be an "e" (the same one!) the next time through the loop, so we'd head back to the start, but the current letter would still be an "e" so we'd head back to the start, and so on....

Go ahead and try this script with something like "hello there". You'll see this:

h
l
l
o
t
h
r

The break Statement Imagine that you hated "e" so much that you didn't even want to see the rest of any word that dared include this horrible letter. You'd rather have the until loop stop completely then go back for another word.

This is where the break statement comes in; it kicks you right out of the current loop. If we change continue in the letter printer script to break, we can remove the extra let statement. We won't need it anymore because we'll be jumping right out of the until loop and continuing with the next word.

Case Study

There's a tricky but very useful statement called case that lets you selectively execute some commands based on a word matching a specified pattern. It's easier to show you a case statement at work than to try to explain it cold, so here's an example.

Byron is writing a shell script that's going to have some options and take a bunch of files as arguments. Byron's one of my cats, so the command syntax used for running his script might end up looking something like this:

apply [ -attitude | -catnip | -catnap | -disdain ] objects

The apply script will apply attitude, some catnip, a cat nap, or some disdain to the objects specified on the command line. If none of these options is specified, the objects are completely ignored and nothing happens; he uses this mode a lot. This setting is used for things like store-bought toys, humans who want to play with the cat, etc. You'll find a few unfamiliar constructs in this script--I'll explain those at the end.

#! /bin/sh
#
# Apply some cat-like behavior to the specified objects.

# First we'll check $# (the number of arguments).

if [ $# -eq 0 ] ; then

# This script is pretty useless with no arguments, so if $#
# is 0, we want to print the usage message and exit.

echo "usage: apply [ action ] objects"
echo "The optional action can be one of:"
echo " attitude"
echo " catnip"
echo " catnap"
echo " disdain"
exit
fi

# The default action is ignoring.

action="ignoring"

# The next line specifies the object's variable without giving
# it a value. We don't have a value for it yet, so we're just
# "initializing" it here. We do this so we can tell if the user
# remembered to include some objects to work on; if not, we
# could remind them.

objects=


# To dig through the options, we're going to loop while $1 (the current
# first option) is set to something; remember, the -n test checks to
# see if a string (in this case, the contents of $1) has one or more
# characters inside.
#
# We use shift to strip off arguments we've already dealt with, so
# $1 could be nothing if we run out of arguments.
#
# Down inside the loop we'll use the break statement when we think
# we've handled all of the options.

while [ -n "$1" ] ; do

# Use case to match the current argument with one of the valid
# options.

# The case statement works like an if statement with a bunch
# of elif clauses; in this case, we check the current argument
# (in $1) against the valid options.

case "$1" in
-attitude)

# Matched the "-attitude" option, so we set the
# action to "attitude" and then dispose of this
# argument with shift. Now $1 will be whatever
# came next on the command line.

action=attitude
shift

# Every pattern in a case statement ends with
# ";;".

;;

-catnip)

# Matched "-catnip".

action=catnip
shift
;;

-catnap | -nap)

# Matched "-catnap" OR "-nap". Why does the
# case statement use | for OR, instead of the ||
# that we saw earlier for combining tests in an
# if statement? Mostly because case is not the
# if statement, and everything has a different
# syntax under Unix.

action=catnap
shift
;;

-disdain)
action=disdain
shift
;;

-*)

# Anything else that starts with - is invalid:

echo "$1 is not a valid option. "
echo "You owe me a cat treat."

# Getting an invalid option is an error, so we
# return an exit status of 1. Any exit status
# that isn't 0 means "Houston, we had a problem."

exit 1
;;

*)

# If we got this far, we're done traveling
# through the options. We'll take all of the
# remaining arguments and store them in the
# objects variable, since these are the objects
# we want to work on.

objects="$*"

# We've handled all of the command-line arguments
# now, so we use break to kick us out of the
# while loop.

break
;;

esac
done

# Let the world know what's happening.

echo Byron will now apply some $action to:
echo $objects

This is a little longer than the rest of the scripts we've seen, but it really saves Byron a lot of time! Now with one command, he can play with things, ignore them, or spread disdain throughout our apartment, leaving much more time for important things like sleeping and eating.

With the exception of the case statement, this is a pretty simple script. What did you expect from a cat? On each trip through the while loop, we examine the current argument (in $1) to see if it's one of the valid options. If it's not a valid option, the script displays an error message and exits. If the argument isn't an option (for this, if it doesn't start with a - it's not an option) we assume that the rest of the arguments are the objects we want to work on.

The form of the case statement is:

case WORD in
PATTERN1)
COMMANDS1
;;

PATTERN2)
COMMANDS2
;;

...
esac

Just as the if statement ends with fi, the case statement ends with esac, even though it looks like a typo.

You may find the similarities and differences between for loops and case statements a bit confusing at first. In for loops, we use a construct like:

for i in *

where * represents all the files in the current directory. But in case statements, we use constructs like:

case argument in
pattern)

where pattern represents a string we want to match. Don't confuse pattern with a range of files, even though they can both have wildcards! We're doing something very different here. The case statement is like an if...elif...else...fi statement.

This is a little more complex than the if statement or the loops we've looked at. The WORD is compared against each of the PATTERNs in the order in which they appear. If WORD matches a PATTERN, the COMMANDS inside that pattern are executed. The patterns have a ")" character at the end, and the list of COMMANDS ends with two ";" characters.

The patterns in a case statement use the same matching rules as shell wildcards (see Chapter 6, The Terminal). A pattern of "*)" will match anything, and is usually the last pattern specified, so that it can handle unexpected values or defaults. Remember, all patterns end with ")".

In the case of Byron's apply script, there are are specific patterns to match all of the valid arguments (such as -attitude or -disdain). Looking at the -catnap handler, you'll see that you can include two or more patterns by separating them with a "|" symbol; this means OR to programmers. This lets you use -catnap or -nap if it's an emergency and you want to type fewer characters.

Why does case use | to create an OR sequence instead of the || we learned about earlier? The | in a pattern just means "this is a list of patterns; you can match any of them." If case used the || syntax, you might think you could also use && (for AND) in a pattern, which isn't possible.

The pattern after the -disdain handler will match anything that starts with a "-" character. If we've gotten this far through the patterns without matching, and something starts with a "-" character, it's an invalid option. The very last pattern will match anything; if we've gotten down here, this isn't an argument, so we must be looking at the first object we want to work on.

The case statement is very popular in GNU configure scripts. These extremely complex scripts are used to automatically query a system to help configure software before it gets compiled into an executable. Part of configure will attempt to guess the type of system you're using and turn it into a string that reflects the operating system, the OS version, and the kind of hardware, such as beos-R4-powerpc or beos-R4-x86. The script then uses case to do some platform-specific configuration:

case SYSTEM in

# Other systems

...

beos-*-powerpc)

# do some BeOS on PowerPC-specific stuff

;;

beos-*-x86)

# do some BeOS on x86-specific stuff

;;

beos-*-*)
echo "Unknown architecture for BeOS"
echo "Very cool, but you might have problems..."
;;

...
esac

Remember the file renaming script we wrote earlier, using the file's type to give it a standard file extension? We can simplify it using the case statement. The original script uses an if...elif...else sequence to assign the extension (assuming the current file's name is in the file variable):

#! /bin/sh
...
if [ "$file_type" = "text/plain" ] ; then
echo "$file is plain text"
mv "$file" "$file.txt"
elif [ "$file_type" = "text/html" ] ; then
echo "$file is HTML"
mv "$file" "$file.html"
else
echo "$file is an unknown file"
rm "$file"
fi

Using case, this becomes a little easier to read (and to extend with new types!):

#! /bin/sh
...
case $file_type in
text/plain)
echo "$file is plain text"
mv "$file" "$file.txt"
;;

text/html)
echo "$file is HTML"
mv "$file" "$file.html"
;;

text/*)
echo "$file is an unknown text file"
rm "$file"
;;

image/*)
echo "$file is an unknown image file"
rm "$file"
;;

*)
echo "$file is an unknown file"
rm "$file"
;;
esac

I've already extended this with two handlers that match any kind of text file (the text/* pattern) and any kind of image file (image/*); this will make the error messages a little more informative. You could extend it to keep all of the JPEG images by adding something like this before the image/* handler:

image/jpeg)
echo "$i" is a JPEG image
mv "$i" "$i.jpg"
;;

It's got to go before the image/* handler, or the image/* handler will match image/jpeg and delete the file for you.

A good rule of thumb is to always put more specific patterns (like text/html or image/jpeg in this example) before more general patterns (like image/* or the match-anything * pattern).

Listening to the User

Complex shell scripts might need some sort of input from the user. Sometimes it's easier just to ask the user a question than to support a million command-line arguments.

The read command reads a line from the standard input stream (which is usually the keyboard, unless you're using a pipe or redirecting from a file; see Chapter 6, The Terminal, for more about pipes and redirection) and assigns it to the REPLY variable. As usual a line is defined as whatever you type until you hit the Enter key.

The read Command You can use the read command's -p option to display a prompt. For example, try typing this into a Terminal window:

$ read -p "Do you like fish? "

After you've entered your answer, which is automatically assigned to the REPLY variable by the shell, type this to see whether you like fish:

echo $REPLY

If you want to assign the input to another variable, include its name as an argument to read:

$ read var_name

You can also combine this with a prompt. The following read command:

$ read -p "Do you like fish? " fishy

will prompt you with "Do you like fish?" and assign your answer to the variable named fishy instead of the REPLY variable.

When more than one variable name is included in a read command, the first word of the input is assigned to the first variable, the second word to the second variable, etc. If there's more input than variables, everything else will be assigned to the last variable. For example, if we type "hello there world" into

$ read greeting rest

$greeting will be given "hello" and "there world" will be assigned to $rest. If there are more variables than words in the input, the extra variables will be empty. Typing only "hi" into the read command above will set $greeting to "hi" and $rest to nothing.

You could use this to extend the file renaming script to ask the user for the type of an unrecognized file. Right now, the script arbitrarily assigns the generic type application/octet-stream:

#! /bin/sh
...
# If there's still no type, give it a generic one:

if [ -z "$file_type" ] ; then
settype -t application/octet-stream "$file"
file_type="application/octet-stream"
fi

Let's change it to use read and ask the user what to do. Replace that if statement with this one:

#! /bin/sh
...
if [ -z "$file_type" ] ; then
echo "$file is an unknown kind of file."
echo "What type of file is it?"
echo "(Just hit return if you don't know.)"
read -p "The filetype is: " file_type

# If they just hit return without entering a type,
# the file_type variable will be empty. The -z test
# is true if a string has no characters.

if [ -z "$file_type" ] ; then
file_type="application/octet-stream"
fi

settype -t $file_type "$file"
fi

Now when an unknown kind of file is encountered, the user will be given some information and prompted for a type:

$ renamer filename
filename is an unknown kind of file.
What type of file is it?
(Just hit return if you don't know.)
The filetype is:

After the read, we check to see if they entered something. If they didn't, we assign the generic filetype. If the user entered a type, we assign that type to the file.

Making Alerts As you saw in Chapter 6, The Terminal, BeOS comes with a command called alert. You can use alert to pop up a dialog box for the user to click on. alert's arguments are

alert [type] text [button1] [button2] [button3]

But what we didn't show you in Chapter 6 is that you can change the type of alert icon displayed in the dialog box. The various types of alerts and their corresponding icons are shown in Table 4.

Table 4 Alert types

Alert Type Description Icon
--empty No alert. None.
--info An informative alert. A blue, 3D "i." This is the default icon.
--idea An idea. A light-bulb.
--warning Something you want to warn the user about. A bright yellow exclamation point.
--stop Something that's very important...the user should stop what they're doing and look at this. A red exclamation point.

Figure 1
Alert types All of the different alert icons being used to write a famous program, "hello world.".

The text is the message you want to appear in the dialog box. You can also specify up to three buttons; if you don't specify any buttons, the alert will have one button labeled OK in it.

If the alert command has any buttons, the command's exit status will be the button number (starting with 0), and the title of the button will be printed on the standard output channel.

Here's an example script demonstrating how to use the alert command's exit status:

#! /bin/sh

# Show the alert, asking the user what they'd prefer
# to drink.
#
# We direct alert's output into the bit-bucket because
# we don't want to see it; for this example, we're using
# alert's exit status.

alert "What would you like?" Coffee Tea Milk > /dev/null

# Store alert's exit status in the button variable;
# the exit status of the last command (which was "alert")
# is stored in the $? variable.

button=$?

# The first button was "Coffee"; if $button equals 0, they
# chose that button.

if [ $button -eq 0 ] ; then
echo "You like coffee."
elif [ $button -eq 1 ] ; then
echo "You like tea."
elif [ $button -eq 2 ] ; then
echo "You like milk."
else
echo "You don't like anything."
fi

Here's the same example script, but using the alert command's output instead of its exit status:

#! /bin/sh

# Show the alert, asking the user what they'd prefer
# to drink.
#
# We direct alert's output into the read statement to store
# the selected button's name in the "button" variable.
#
# You could also do this:
#
# button=$(alert "What would you like?" Coffee Tea Milk)
#
# These techniques both give you exactly the same results...use
# whichever one you prefer.

alert "What would you like?" Coffee Tea Milk | read button

# Now we compare the selected button to see what the user
# picked. Note how we're using strings now; alert prints the text
# of the selected button, and we've used read to store that text
# in the "button" variable.

if [ "$button" = "Coffee" ] ; then
echo "You like coffee."
elif [ "$button" = "Tea" ] ; then
echo "You like tea."
elif [ "$button" = "Milk" ] ; then
echo "You like milk."
else
echo "You don't like anything."
fi

Using the button string is easier than dealing with the exit status, though it can be easy to miss the read command that's saving the button text into a variable for us.

Using Functions

As your scripts become more complex, you'll find yourself looking for ways to keep things clear and organized, and to treat sections of your scripts like "objects" that can be invoked from other sections. That's what functions are all about.

Functions in Scripts Take a look at the lowly echo command:

echo 'Hello World'

If you place this line in your script, "Hello World" is printed to the screen when that line is encountered. But what if your script needs to do this dozens of times, from different places? And what if instead of just a one-line echo command, you wanted to invoke a whole series of commands? You could turn that block of commands into a "function," like this:

#! /bin/sh

hello() {
echo "Hello World"
ls -l > /boot/home/dirlist.txt
cat /boot/home/dirlist.txt
}

Save the block above to a script and run it--and nothing will happen. Functions don't run themselves! In this case, you have only declared, but not yet invoked the function called hello. In order to make a declared function run, just enter its name on a blank line below the function. If you put hello on a line below the function above, then run the script again, your echo command and the other commands will be processed. This will become a very important concept as you start to build complex scripts, since it lets you define scripts within scripts, surround them in the function construct, and then invoke them from elsewhere in your scripts. In essence, it gives your scripts a small degree of "object-oriented" behavior.

You must always declare your functions before invoking them. Try to do it the other way around, and your script will fail with error messages.

For example, you might want to structure a complex script like this:

#! /bin/sh
PartOne() {
BLOCK OF COMMANDS
}

PartTwo() {
MORE COMMANDS
PartThree
A FEW MORE COMMANDS
}

PartThree() {
YET MORE COMMANDS
}

# Now that our functions have all been declared, we can invoke them

PartOne
PartTwo

Note that we didn't invoke PartThree from the bottom of the script, but from within PartTwo. When the script runs, it will run PartOne, then the beginning of PartTwo, then PartThree as a "subroutine" of PartTwo, then finish up the rest of the commands in PartTwo. This structure lets you branch off from one point in your script to another, and then return to where you left off to do some more work.

Functions in the Shell In addition to using functions inside your scripts, you can also store them in memory and invoke them directly from the command line. For example, if you've got a set of commands that you use all the time, and want to be able to use them as quickly as possible without having to load another file from the disk, you can store functions in your /boot/home/.profile. This way they'll be loaded into memory whenever you launch a Terminal session, and can be run at any time, either directly from the command line or by invoking them from other scripts.

Shell functions can process command-line arguments just like a shell script:

#! /bin/sh

showargs() {
echo "There are $# arguments:"

for arg in "$@" ; do
echo "$arg"
done
}

This new showargs command isn't really that useful, but it's easy to use the command-line arguments in a function for something useful. They'll be needed any time you move a script that uses command-line arguments into a shell function.

Shell functions are specific to one script, and they won't leak out into your shell sessions; as soon as the shell exits, they're gone. The functions you define in your .profile are always available in a Terminal because the shell that loaded .profile doesn't exit until you close that Terminal window.

For example, the renamer script uses the same slightly nasty-looking command several times to get the MIME type for a file:

#! /bin/sh
...
file_type=$(catattr BEOS:TYPE "$file" 2> /dev/null | \
awk '{ print $5; }')
...

Instead of typing this in several times, we could turn it into a shell function by putting this into the script before the for loop starts running through the arguments:

#! /bin/sh
...
get_type() {

# Note how we use $1 to get the first argument in our
# function; the $file variable we used above only makes
# sense to commands in the for loop. The function stands
# alone by itself, so it has to work with its arguments
# instead.

catattr BEOS:TYPE "$1" 2> /dev/null | \
awk '{ print $5; }'
}
...

Now we can change the line used to get the filetype to:

#! /bin/sh
...
file_type=$(get_type "$file")
...

This makes things a lot easier to read!

Shell functions exit by running though the last command in the function or any time they hit a return statement:

#! /bin/sh

do_something() {
if something_bad ; then

# Return NOW with an exit status of 1 (i.e., an error).

return 1


# More commands in the function...

# At the end of the function we return with the exit status
# of whatever the last command in the function was.
}
...

The return statement exits the shell function and sends its argument back to the shell as an exit status. If you use return without an argument, it sends the return value of the last command as the exit status.

The return statement for functions is just like the exit statement for shell scripts; the difference is that return just ends the function, and exit ends the entire script.

Exit Status Remember, an exit status of 0 means success or true, and an exit status of anything else means failure or false. The exit status of the last command is available in the special variable $? and you should save this in another variable if you need to do complex tests on it. To save the value of one variable into another, use something like:

#! /bin/sh
...
ReturnHolder=$?
...

$ReturnHolder will then contain the value of whatever $? was, freeing up $? to take another value later on. Use return to send back an exit status from a shell function or exit to send back an exit status from a shell script.

Debugging Your Scripts

If you've got a bug or a typo in one of your shell scripts, it can be pretty hard to figure out what's going on. Even if you do get a readable error message, it might be telling you that the error is at a perfectly valid line in the script.

You can see each line of a shell script as it's executed by adding set -x at the top of a shell script. For example, say you've got our original test script with set -x at the top:

#! /bin/sh
set -x
echo hello world

When you run this, you'll see

+ echo hello world
hello world

Each line of the script (after the set -x) gets printed prefixed by + and a space.

You can also add -x to the /bin/sh magic cookie at the start of the script; this is exactly the same as using the set -x command:

#! /bin/sh -x
echo hello world


<< previous
^ top ^
next >>

Readers-Only Access
Scripting
Games
Emulation
Hardware and Peripherals
The Kits
The Future
About BeOS | Online Chapters | Interviews | Updates

Please direct technical questions about this site to webmaster@peachpit.com.

Copyright © 1999 Peachpit Press and the respective authors.