sections in this module City College of San Francisco - CS160B
Unix/Linux Shell Scripting
Module: Functions
module list

Examples

Some of the files containing examples are in the examples-shotts/functions directory beneath the class work area on hills.

Example 1:

You maintain files of contacts in your home directory. A contacts file consists of one line per contact with three fields separated by tabs: the contact's name, their phone number and their email address.

Write a function to add a contact to a contacts file. It should have this synopsis:

newcontact file

where file is the path to the contacts file. Your function should ask the user for the contact information, then append the contact to the file

Example 2:

Given the following file, named addresses

"Greg Boyd" "444 First St." San Jose CA
Eulinda\ Schmaltz Unknown\ Address San Francisco CA

and the following commands

read name1 address1 city1 < addresses
read name2 address2 city2 < addresses

what is the value of each of the variables?

Can you rewrite this example (both the file addresses and the code that reads them) so that the variables are set correctly? (This is tricky.)

Example 3:

Consider the following shell script getcontact

#!/bin/bash
#
#
addcontact() {

[ ! -f "$1" -o ! -w "$1" ] && return 1
read -p "Enter your name:" name

read -p "Enter your phone number:" phone
read -p "Enter your email:" email
line="$name\t$phone\t$email"
echo -e "$line" >> "$1"
return 0
}
read -p "Enter the filename:" line
addcontact "$line"
# cat the resulting file
cat "$line"

getcontact has a bug. Can you find it? Explain the problem and how to fix it.

Example 4 has been moved to different section.

Example 5:

As you know, the command 

mv source target

functions very differently depending on whether target exists. Write a "safe rename" command (shell script) to rename a file or directory (source) only if the target does not exist:

safemv source target

so long as target does not exist, safemv will rename anything (file, directory, etc). If savemv encounters an error or invalid arguments, it must output an appropriate error message to stderr and exit with an error.

Example 6:

Write a command diff to output the difference of two integers expressed as a positive number. Thus

diff 4 5

and 

diff 5 4

both output 1

Your command must check its arguments to ensure they "look like" integers. If either is invalid, diff must output an error message to stderr and exit with an error. (Note: diff is a standard command. If you write this command, you will have to execute it like this: ./diff Otherwise, you will be running the Unix command diff, which is used to compare two text files.)

Answers

Example 1:

Your function will take one argument, which must be present and be a writable file. It will then interact with the user to get the contact information and append the result to the file. We will just accept whatever information the user types in: there will be no checking the format of the fields:

newcontact() {
# add a contact to the contacts file $1
# ensure there is a single argument
if [ $# -ne 1 ]; then
echo "newcontact: single argument required" >&2
return 1
fi
# $1 must be a writable file

if [ ! -f "$1" -o ! -w "$1" ]; then
echo "newcontact: single argument '$1' - not a writable file" >&2
return 1
fi
read -p "Enter the contact name:" name
read -p "Enter the contact phone number:" phone
read -p "Enter the contact email address:" email
echo -e "$name\t$phone\t$email" >> "$1"
}

Example 2:

The value of name1 is the same as the value of name2; the value of address1 is the same as that of address2, etc.  This is because the redirection operator opens the file each time it is executed, so each read command reads the first line of addresses. Let's fix that problem first. This means the commands can only open addresses once. 

We could place the commands in a shell script, then redirect to the shell script instead. We'll create our shell script readaddresses with the commands

read name1 address1 city1 
read name2 address2 city2 

we could then execute readaddresses like this:

readaddresses < addresses

The first line of addresses would then be read by the first read command, the second by the second read command. We could also do this using a function

readaddresses() {
read name1 address1 city1 
read name2 address2 city2 
}

and then redirect to the function like this

readaddresses < addresses

But we can get trickier still by using an anonymous function, which is also called a block (or in Swift, a closure):

{
read name1 address1 city1 
read name2 address2 city2 
} < addresses

Any of these techniques would work. However, the addresses file still has some problems. Let's see what the result is if we execute our read commands as a block on the current file whose contents is below:

"Greg Boyd" "444 First St." San Jose CA
Eulinda\ Schmaltz Unknown\ Address San Francisco CA

Here are the values of the resulting variables:

-bash$ echo $name1
"Greg
-bash$ echo $address1
Boyd"
-bash$ echo $city1
"444 First St." San Jose CA
-bash$ echo $name2
Eulinda Schmaltz
-bash$ echo $address2
Unknown Address
-bash$ echo $city2
San Francisco CA
-bash$


Thus we see that the read command treated the quotation mark as ordinary text, but it understood the backslash! (The value of city2 was correct because it was the last variable and the remainder of the line contained the city.)

Let's modify our addresses file so that it works correctly:

Greg\ Boyd 444\ First\ St. San Jose CA
Eulinda\ Schmaltz Unknown\ Address San Francisco CA

Example 3:

Referring back to our shell script getcontact

#!/bin/bash
#
#
addcontact() {

[ ! -f "$1" -o ! -w "$1" ] && return 1
read -p "Enter your name:" name

read -p "Enter your phone number:" phone
read -p "Enter your email:" email
line="$name\t$phone\t$email"
echo -e "$line" >> "$1"
return 0
}
read -p "Enter the filename:" line
addcontact "$line"
# cat the resulting file
cat "$line"

Here's what happens when we run getcontact:

-bash$ getcontact
Enter the filename:foo
Enter your name:Greg
Enter your phone number:4444444
Enter your email:greg@greg.com
cat: Cannot open Greg\t4444444\tgreg@greg.com: No such file or directory
-bash$

The problem has to do with the variable line. Remember, by default all variables in a function are global. This means that the variable line in the function is the same variable as the variable line in the surrounding shell program. The alteration of the global variable line by the function is called a side-effect. To avoid it you should declare all variables used for temporary storage in a function local. When we add the following line as the first line of the function addcontact():

local line name phone email

the program executes correctly.

Example 4 has been moved to different section


Example 5:

We are supposed to write a "safe rename" command (shell script) to rename a file or directory (source) only if the target does not exist:

safemv source target

As always, the meat of this problem is easy - it is just a mv command. The complexity is in the details. To satisfy our requirements, we should ask the following questions when we start. We want to answer each of these questions as "yes" in order to be successful.

There are also a lot of permissions issues to address. These can be very complicated and it is very easy to miss a requirement. To be safe, we would have to test that the first argument is readable, that its directory is writable, and that the directory of the second argument is writable. (I don't know about you, but I'm tired already.) For this reason, shell scripts (and Unix programs as well) often take a different approach:

Here's our code. (See the examples directory on hills).

#!/bin/bash
#
#       safemv source dest
#
# performs a mv operation 'safely', i.e., on these conditions:
#       - the source exists
#       - the destination does not
#
# This command might rather be called 'saferename',
#
#
fatal() {
    # fatal: output an error message and a synopsis message to standard error.
    # then exit with an error.
    local prog=$(basename "$0")
    echo "$prog: ERROR - $*" >&2
    echo "usage: $(basename "$0") source destination" >&2
    exit 1
}

[ $# -ne 2 ] && fatal "need exactly two arguments"

# check for existance of the first argument
[ ! -e "$1" ] && fatal "'$1' does not exist"

# check for non-existance of the second argument
[ -e "$2" ] && fatal "'$2' exists - safemv will not delete or rearrange"

# try the mv operation and see if it succeeds. We
# will suppress the error message output by mv. You may not want
# to do that.
if ! mv "$1" "$2" 2>/dev/null; then
    fatal "rename failed - check directory permissions."
fi

Example 6:

Conceptually, the solution for 

diff num1 num2

is simple. The problem comes with this restriction:

Your command must check its arguments to ensure they "look like" integers.

How do we go about doing this? The solution is that most-dreaded of words, regular expression.

Regular expressions are your friends. They provide an exact, compact, method to look for data, or, in our case, to validate input, using a pattern. For us, we will take a variable containing the [candidate] integer, echo its contents to grep and ask grep to tell is if it matches a pattern. Let's ask some questions to create our pattern:

Here's our answers:

This last requirement causes some problems, as there is no way in a basic regular expression to indicate the minus sign is optional. We have two choices:We will use the basic regular expressions in our code. Now that we have constructed our regular expression(s), how do we want to use them? The easiest way to write code in your shell script to check the arguments is to write a sequence of simple tests like this:

Our regular expression checks to see if our variable is an integer, so we want to see if the grep command fails. This calls for a negation symbol. But we will be using a pipeline of commands to test the integer. This raises two interesting questions that may not yet be obvious. Suppose we have a pipelined command

cmd1 | cmd2

  1. If cmd1 succeeds but cmd2 doesnt, what is the exit status of the entire command? 
  2. If we place a ! in front of the pipeline, does it apply to the exit status of the entire [pipelined] command or just to the first command in the pipeline?
  1. The exit status of a pipeline is determined by the exit status of the last command in the pipeline.
  2. A ! in front of a pipeline applies to the exit status of the entire pipeline.

Now that we've covered those issues, here is a command to test $num1 to see if it's an integer:

if ! echo "$num1" | grep -q -e "^[0-9][0-9]*$" -e "^-[0-9][0-9]*$"; then
    echo "'$num1' is not an integer"
fi

(Remember, the -q option to grep has the same effect as redirecting grep's standard output to /dev/null.)

You can also do this using an extended regular expression with the [[ ]] operator:

if ! [[ "$num1" =~ ^-?[0-9]+$ ]]; then

echo "'$num1' is not an integer"

fi

And here is our code for diff:

#!/bin/bash
#
#
#       diff num1 num2
#
# output the absolute value of (num1 - num2).
# num1 and num2 must be integers. They may be negative
#
fatal() {
    # fatal: output an error message and a synopsis message to standard error.
    # then exit with an error.
    local prog=$(basename "$0")
    echo "$prog: ERROR - $*" >&2
    echo "usage: $(basename "$0") num1 num2" >&2
    exit 1
}

[ $# -ne 2 ] && fatal "need exactly two arguments"

num1=$1
num2=$2
if ! echo "$num1" | grep -q -e "^[0-9][0-9]*$" -e "^-[0-9][0-9]*$"; then
        fatal "'$num1' is not an integer"
fi

if ! echo "$num2" | grep -q -e "^[0-9][0-9]*$" -e "^-[0-9][0-9]*$"; then
        fatal "'$num2' is not an integer"
fi

((result=num1-num2))
if [ "$result" -lt 0 ]; then
        ((result=0-result))
fi
echo $result

Remember, diff is a standard Unix command, so you will have to run this command as 

./diff 4 5

A different version of diff

We can make our code even more readable by moving our code that checks for an integer into a Boolean function. The idea is to create a function that succeeds or fails depending on whether its single argument "looks like" an integer. In our case, we could get rid of the ugly regular expressions from our main code:

if ! echo "$num1" | grep -q -e "^[0-9][0-9]*$" -e "^-[0-9][0-9]*$"; then
        fatal "'$num1' is not an integer"
fi

by creating a function isint(). The result is much prettier:

if ! isint "$num1" ; then
        fatal "'$num1' is not an integer"
fi

or, better,

isint "$num1" || fatal "'$num1' is not an integer"

Here is our code after creation of the function isint(). Notice that the function is 'safe', since it uses only its arguments and only sets its exit status:

#!/bin/bash
#
#
#       diff num1 num2
#
# output the absolute value of (num1 - num2).
# num1 and num2 must be integers. They may be negative
#
fatal() {
    # fatal: output an error message and a synopsis message to standard error.
    # then exit with an error.
    local prog=$(basename "$0")
    echo "$prog: ERROR - $*" >&2
    echo "usage: $(basename "$0") num1 num2" >&2
    exit 1
}

isint() {
# isint - succeed or fail depending on whether the single argument
# 'looks like' an integer
echo "$1" | grep -q -e "^[0-9][0-9]*$" -e "^-[0-9][0-9]*$"
}

[ $# -ne 2 ] && fatal "need exactly two arguments"

num1=$1
num2=$2

isint "$num1" || fatal "'$num1' is not an integer"

isint "$num2" || fatal "'$num2' is not an integer"

((result=num1-num2))
if [ "$result" -lt 0 ]; then
        ((result=0-result))
fi
echo $result

This last version is named diff2. It can be found in the examples area.

Prev This page was made entirely with free software on linux:  
Kompozer
and Openoffice.org    
Next

Copyright 2011 Greg Boyd - All Rights Reserved.

Document made with Kompozer