Chapter 13. Loose Ends

We've put commands and features into this chapter that for one reason or another did not logically fit into earlier chapters. There's no particular rationale for their order of presentation.

The eval Command

This section describes another of the more unusual commands in the shell: eval. Its format is as follows:

eval command-line

where command-line is a normal command line that you would type at the terminal. When you put eval in front of it, however, the net effect is that the shell scans the command line twice before executing it.[1] For the simple case, this really has no effect:

$ eval echo hello
hello
$

But consider the following example without the use of eval:

$ pipe="|"
$ ls $pipe wc -l
|: No such file or directory
wc: No such file or directory
-l: No such file or directory
$

Those errors come from ls. The shell takes care of pipes and I/O redirection before variable substitution, so it never recognizes the pipe symbol inside pipe. The result is that the three arguments |, wc, and -l are passed to ls as arguments.

Putting eval in front of the command sequence gives the desired results:

$ eval ls $pipe wc –l
     16
$

The first time the shell scans the command line, it substitutes | as the value of pipe. Then eval causes it to rescan the line, at which point the | is recognized by the shell as the pipe symbol.

The eval command is frequently used in shell programs that build up command lines inside one or more variables. If the variables contain any characters that must be seen by the shell directly on the command line (that is, not as the result of substitution), eval can be useful. Command terminator (;, |, &), I/O redirection (<, >), and quote characters are among the characters that must appear directly on the command line to have any special meaning to the shell.

For the next example, consider writing a program last whose sole purpose is to display the last argument passed to it. You needed to get at the last argument in the mycp program in Chapter 10, “Reading and Printing Data.” There you did so by shifting all the arguments until the last one was left. You can also use eval to get at it as shown:

$ cat last
eval echo $$#
$ last one two three four
four
$ last *                        Get the last file
zoo_report
$

The first time the shell scans

echo $$#

the backslash tells it to ignore the $ that immediately follows. After that, it encounters the special parameter $#, so it substitutes its value on the command line. The command now looks like this:

echo $4

(the backslash is removed by the shell after the first scan). When the shell rescans this line, it substitutes the value of $4 and then executes echo.

This same technique could be used if you had a variable called arg that contained a digit, for example, and you wanted to display the positional parameter referenced by arg. You could simply write

eval echo $$arg

The only problem is that just the first nine positional parameters can be accessed this way; to access positional parameters 10 and greater, you must use the ${n} construct:

eval echo ${$arg}

Here's how the eval command can be used to effectively create “pointers” to variables:

$ x=100
$ ptrx=x
$ eval echo $$ptrx               Dereference ptrx
100
$ eval $ptrx=50                  Store 50 in var that ptrx points to
$ echo $x                        See what happened
50
$

The wait Command

If you submit a command line to the background for execution, that command line runs in a subshell independent of your current shell (the job is said to run asynchronously). At times, you may want to wait for the background process (also known as a child process because it's spawned from your current shell—the parent) to finish execution before proceeding. For example, you may have sent a large sort into the background and now want to wait for the sort to finish because you need to use the sorted data.

The wait command is for such a purpose. Its general format is

wait process-id

where process-id is the process id number of the process you want to wait for. If omitted, the shell waits for all child processes to complete execution. Execution of your current shell will be suspended until the process or processes finish execution. You can try the wait command at your terminal:

$ sort big-data > sorted_data &           Send it to the background
[1] 3423                                  Job number & process id from the shell
$ date                                    Do some other work
Wed Oct  2 15:05:42 EDT 2002
$ wait 3423                               Now wait for the sort to finish
$                                         When sort finishes, prompt is returned

The $! Variable

If you have only one process running in the background, then wait with no argument suffices. However, if you're running more than one command in the background and you want to wait on a particular one, you can take advantage of the fact that the shell stores the process id of the last command executed in the background inside the special variable $!. So the command

wait $!

waits for the last process sent to the background to complete execution. As mentioned, if you send several commands to the background, you can save the value of this variable for later use with wait:

prog1 &
pid1=$!
...
prog2 &
pid2=$!
...
wait $pid1          # wait for prog1 to finish
...
wait $pid2          # wait for prog2 to finish

The trap Command

When you press the Delete[2] or Break key at your terminal during execution of a shell program, normally that program is immediately terminated, and your command prompt returned. This may not always be desirable. For instance, you may end up leaving a bunch of temporary files that won't get cleaned up.

The pressing of the Delete key at the terminal sends what's known as a signal to the executing program. The program can specify the action that should be taken on receipt of the signal. This is done with the trap command, whose general format is

trap commands signals

where commands is one or more commands that will be executed whenever any of the signals specified by signals is received.

Numbers are assigned to the different types of signals, and the more commonly used ones are summarized in Table 13.1. A more complete list is given under the trap command in Appendix A, “Shell Summary.”

Table 13.1. Commonly Used Signal Numbers

Signal

Generated for

0

Exit from the shell

1

Hangup

2

Interrupt (for example, Delete, Ctrl+c key)

15

Software termination signal (sent by kill by default)

As an example of the trap command, the following shows how you can remove some files and then exit if someone tries to abort the program from the terminal:

trap "rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit" 2

From the point in the shell program that this trap is executed, the two files work1$$ and dataout$$ will be automatically removed if signal number 2 is received by the program. So if the user interrupts execution of the program after this trap is executed, you can be assured that these two files will be cleaned up. The exit that follows the rm is necessary because without it execution would continue in the program at the point that it left off when the signal was received.

Signal number 1 is generated for hangup: Either someone intentionally hangs up the line or the line gets accidentally disconnected. You can modify the preceding trap to also remove the two specified files in this case by adding signal number 1 to the list of signals:

trap "rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit"  1 2

Now these files will be removed if the line gets hung up or if the Delete key gets pressed.

The commands specified to trap must be enclosed in quotes if they contain more than one command. Also note that the shell scans the command line at the time that the trap command gets executed and also again when one of the listed signals is received. So in the preceding example, the value of WORKDIR and $$ will be substituted at the time that the trap command is executed. If you wanted this substitution to occur at the time that either signal 1 or 2 was received (for example, WORKDIR may not have been defined yet), you can put the commands inside single quotes:

trap 'rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit'  1 2

The trap command can be used to make your programs more user friendly. In the next chapter, when we revisit the rolo program, the signal generated by the Delete key is caught by the program and brings the user back to the main menu. In this way, this key can be used to abort the current operation without exiting from the program.

trap with No Arguments

Executing trap with no arguments results in the display of any traps that you have changed.

$ trap 'echo logged off at $(date) >>$HOME/logoffs' 0
$ trap                            List changed traps
trap – 'echo logged off at $(date) >>$HOME/logoffs' EXIT
$ Ctrl+d                           Log off
login: steve                      Log back in
Password:
$ cat $HOME/logoffs               See what happened
logged off at Wed Oct  2 15:11:58 EDT 2002
$

A trap was set to be executed whenever signal 0 was received by the shell. This signal is generated whenever the shell is exited. Because this was set in the login shell, the trap will be taken when you log off. The purpose of this trap is to write the time you logged off into the file $HOME/logoffs. The command is enclosed in single quotes to prevent the shell from executing date when the trap is defined.

The trap command is then executed with no arguments, which results in the display of the changed action to be taken for signal 0 (EXIT). Next, steve logs off and then back on again to see whether the trap works. Displaying the contents of $HOME/logoffs verifies that the echo command was executed when steve logged off.

Ignoring Signals

If the command listed for trap is null, the specified signal will be ignored when received. For example, the command

trap "" 2

specifies that the interrupt signal is to be ignored. You might want to ignore certain signals when performing some operation that you don't want interrupted.

Note that the first argument must be specified for a signal to be ignored and is not equivalent to writing the following, which has a separate meaning of its own:

trap 2

If you ignore a signal, all subshells also ignore that signal. However, if you specify an action to be taken on receipt of a signal, all subshells will still take the default action on receipt of that signal. For the signals we've described, this means that the subshells will be terminated.

Suppose that you execute the command

trap "" 2

and then execute a subshell, which in turn executes other shell programs as subshells. If an interrupt signal is then generated, it will have no effect on the shells or subshells that are executing because they will all ignore the signal.

If instead of executing the previous trap command you execute

trap : 2

and then execute your subshells, then on receiving the interrupt signal the current shell will do nothing (it will execute the null command), but all active subshells will be terminated (they will take the default action—termination).

Resetting Traps

After you've changed the default action to be taken on receipt of a signal, you can change it back again with trap if you simply omit the first argument; so

trap 1 2

resets the action to be taken on receipt of signals 1 or 2 back to the default.

More on I/O

You know about the standard constructs <, >, and >> for input redirection, output redirection, and output redirection with append, respectively. You also know that you can redirect standard error from any command simply by writing

command 2> file

Sometimes you may want to explicitly write to standard error in your program. You can redirect the standard output for a command to standard error by writing

command >&  2

The notation >& specifies output redirection to a file associated with the file descriptor that follows. File descriptor 0 is standard input, descriptor 1 is standard output, and descriptor 2 is standard error. Note that no space is permitted between the > and the &.

So to write an error message to standard error, you write

echo "Invalid number of arguments" >&  2

Frequently, you may want to collect the standard output and the standard error output from a program into the same file. If you know the name of the file, this is straightforward enough:

command >foo 2>>foo

Here, both the standard output and the standard error output from command will be written to foo.

You can also write

command >foo 2>&1

to achieve the same effect; standard output is redirected to foo, and standard error is redirected to standard output (which has already been redirected to foo). Note that because the shell evaluates redirection from left to right on the command line, the last example cannot be written

command 2>&1 > foo

because this would first redirect standard error to standard output (your terminal by default) and then standard output to foo.

You recall that you can also dynamically redirect standard input or output in a program using the exec command:

exec < datafile

redirects standard input from the file datafile. Subsequent commands executed that read from standard input will read from datafile instead. The command

exec > /tmp/output

does the same thing with standard output: All commands that subsequently write to standard output will write to /tmp/output (unless explicitly redirected elsewhere). Naturally, standard error can be reassigned this way as well:

exec 2> /tmp/errors

Here, all output to standard error will go to /tmp/errors.

<&- and >&-

The characters >&- have the effect of closing standard output. If preceded by a file descriptor, the associated file is closed instead. So writing (the impractical)

ls >&-

causes the output from ls to go nowhere because standard output is closed by the shell before ls is executed.

The same thing applies for input using <&-.

$ wc <&-
      0   0   0
$

Inline Input Redirection

If the << characters follow a command in the format

command <<word

the shell uses the lines that follow as the standard input for command, until a line that contains just word is found. Here's a small example at the terminal:

$ wc -l <<ENDOFDATA            Use lines up to ENDOFDATA as standard input
> here's a line
> and another
> and yet another
> ENDOFDATA
      3
$

Here the shell fed every line typed into the standard input of wc until it encountered the line containing just ENDOFDATA.

Inline input redirection is a powerful feature when used inside shell programs. It lets you specify the standard input to a command directly in the program, thus obviating the need to write it into a separate file first, or to use echo to get it into the standard input of the command.

$ cat mailmsg
mail $* <<END-OF-DATA

Attention:

Our monthly computer users group meeting
will take place on Friday, October 4, 2002 at
8am in Room 1A-308. Please try to attend.

END-OF-DATA
$

To execute this program for all members of the group that are contained in the file users_list, you could write

mailmsg $(cat users_list)

The shell performs parameter substitution for the redirected input data, executes back-quoted commands, and recognizes the backslash character. However, any other special characters, such as *, |, and ", are ignored. If you have dollar signs, back quotes, or backslashes in these lines that you don't want interpreted by the shell, you can precede them with a backslash character. Alternatively, if you want the shell to leave the input lines completely untouched, you can precede the word that follows the << with a backslash.

$ cat <<FOOBAR
> $HOME
> *****
>     $foobar
> `date`
> FOOBAR                     Terminates the input
/users/steve
*****
    $foobar
Wed Oct  2 15:23:15 EDT 2002
$

Here the shell supplies all the lines up to FOOBAR as the input to cat. It substitutes the value for HOME but not for foobar because it's preceded by a backslash. The date command is also executed because back quotes are interpreted.

$ cat <<FOOBAR
> \\
> `date`
> $HOME
> FOOBAR
\\
`date`
$HOME
$

The backslash before FOOBAR tells the shell to leave the following lines alone. So it ignores the dollar signs, backslashes, and back quotes.

Use care when selecting the word that follows the <<. Generally, just make sure that it's weird enough so that the chances of it accidentally appearing in the following lines are remote.

If the first character that follows the << is a dash (-), leading tab characters in the input will be removed by the shell. This is useful for visually indenting the redirected text.

$ cat <<-END
>           Indented lines
>           So there you have it
> END
Indented lines
So there you have it
$

Shell Archives

One of the best uses of the inline input redirection feature is for creating shell archive files. With this technique, one or more related shell programs can be put into a single file and then shipped to someone else using the standard Unix mail commands. When the archive is received, it can be easily “unpacked” by simply running the shell on it.

For example, here's an archived version of the lu, add, and rem programs used by rolo:

$ cat rolosubs
#
# Archived programs used by rolo.
#

echo Extracting lu
cat >lu <<THE-END-OF-DATA
#
# Look someone up in the phone book
#

if [ "$#" -ne 1 ]
then
        echo "Incorrect number of arguments"
        echo "Usage: lu name"
        exit 1
fi

name=$1
grep "$name" $PHONEBOOK

if [ $? -ne 0 ]
then
        echo "I couldn't find $name in the phone book"
fi
THE-END-OF-DATA

echo Extracting add
cat >add <<THE-END-OF-DATA
#
# Program to add someone to the phonebook file
#

if [ "$#" -ne 2 ]
then
        echo "Incorrect number of arguments"
        echo "Usage: add name number"
        exit 1
fi

echo "$1    $2" >> $PHONEBOOK
sort -o $PHONEBOOK $PHONEBOOK
THE-END-OF-DATA

echo Extracting rem
cat >rem <<THE-END-OF-DATA
#
# Remove someone from the phone book
#

if [ "$#" -ne 1 ]
then
        echo "Incorrect number of arguments"
        echo "Usage: rem name"
        exit 1
fi

name=$1

#
# Find number of matching entries
#

matches=$(grep "$name" $PHONEBOOK | wc –l)

#
# If more than one match, issue message, else remove it
#

if [ "$matches" -gt 1 ]
then
        echo "More than one match; please qualify further"
elif [ "$matches" -eq 1 ]
then
        grep -v "$name" $PHONEBOOK > /tmp/phonebook
        mv /tmp/phonebook $PHONEBOOK
else
        echo "I couldn't find $name in the phone book"
fi
THE-END-OF-DATA
$

To be complete, this archive should probably include rolo as well, but we didn't here to conserve space.

Now you have one file, rolosubs, that contains the source for the three programs lu, add, and rem, which can be sent to someone else using mail:

$ mail [email protected] < rolosubs             Mail the archive
$ mail [email protected]                        Mail tony a message
Tony,
     I mailed you a shell archive containing the programs
     lu, add, and rem. rolo itself will be sent along shortly.
Pat
Ctrl+d
$

When tony receives the file in his mail, he can extract the three programs simply by running the shell on the file (after having first removed some header lines that mail sticks at the beginning of the file):

$ sh rolosubs
Extracting lu
Extracting add
Extracting rem
$ ls lu add rem
add
lu
rem
$

The shar program used to create the rolosubs archive file is simple:

$ cat shar
#
# Program to create a shell archive
# from a set of files
#

echo "#"
echo "# To restore, type sh archive"
echo "#"

for file
do
   echo
   echo "echo Extracting $file"
   echo "cat >$file <<THE-END-OF-DATA
   cat $file
   echo "THE-END-OF-DATA"
done

Refer to the contents of the rolosubs file when studying the operation of this shar program. Remember, shar actually creates a shell program.

More sophisticated archiving programs allow entire directories to be archived and also check to make sure that no data is lost in the transmission (see Exercises 2 and 3 at the end of this chapter). The Unix sum command can be used to generate a checksum for a program. This checksum can be generated on the sending end for each file in the archive, and then commands included in the shell archive can verify the sum on the receiving end. If they don't match, an error message can be displayed.

Functions

The POSIX standard shell supports the concept of functions; note that older shells may not support this feature.

To define a function, you use the general format:

name () { command; ... command; }

where name is the name of the function, the parentheses denote to the shell that a function is being defined, and the commands enclosed between the curly braces define the body of the function. These commands will be executed whenever the function is executed. Note that at least one whitespace character must separate the { from the first command, and that a semicolon must separate the last command from the closing brace if they occur on the same line.

The following defines a function called nu that displays the number of logged-in users:

nu () { who | wc -l; }

You execute a function the same way you execute an ordinary command: simply by typing its name to the shell:

$ nu
     22
$

Arguments listed after the function on the command line are assigned to the positional parameters $1, $2, ..., just as with any other command. Here's a function called nrrun that runs tbl, nroff, and lp on the file given as its argument:

$ nrrun () { tbl $1 | nroff -mm -Tlp | lp; }
$ nrrun memo1                     Run it on memo1
request id is laser1-33 (standard input)
$

Functions exist only in the shell in which they're defined; that is, they can't be passed down to subshells. Further, because the function is executed in the current shell, changes made to the current directory or to variables remain after the function has completed execution:

$ db () {
>       PATH=$PATH:/uxn2/data
>       PS1=DB:
>       cd /uxn2/data
>       }
$ db                               Execute it
DB:

As you see, a function definition can continue over as many lines as necessary. The shell displays your secondary command prompt until you close the definition with the }.

You can put definitions for commonly used functions inside your .profile so that they'll be available whenever you log in. Alternatively, you can group the definitions in a file, say myfuncs, and then execute the file in the current shell by typing

. myfuncs

This has the effect of causing any functions defined inside myfuncs to be read in and defined to the current shell.

The following function, called mycd, takes advantage of the fact that functions are run in the current environment. It mimics the operation of the Korn shell's cd command, which has the capability to substitute portions of the current directory's path with something else (see the discussion of cd in Chapter 15, “Interactive and Nonstandard Shell Features,” for more details).

$ cat myfuncs                      See what's inside
#
# new cd function:
#      mycd dir Switches dir
#      mycd old new  Substitute new for old in current directory's path
#
mycd ()
{
        if [ $# -le 1 ]
        then
                # normal case -- 0 or 1 argument
                cd $1
        elif [ $# -eq 2 ]
        then
                # special case -- substitute $2 for $1
                cd $(echo $PWD | sed "s|$1|$2|")
        else
                # cd can't have more than two arguments
                echo mycd: bad argument count
                exit 1
        fi
}

$ . myfuncs                   Read in definition
$ pwd
/users/steve
$ mycd /users/pat             Change directory
$ pwd                         Did it work?
/users/pat
$ mycd pat tony               Substitute tony for pat
$ pwd
/users/tony
$

After a function has been defined, its execution will be faster than an equivalent shell program file. That's because the shell won't have to search the disk for the program, open the file, and read its contents into memory.

Another advantage of functions is the capability to group all your related shell programs in a single file if desired. For example, the add, lu, and rem programs from Chapter 11, “Your Environment,” can be defined as functions inside rolo. The template for such an approach is shown:

$ cat rolo
#
# rolo program written in function form
#

#
# Function to add someone to the phonebook file
#

add () {
        # put commands from add program here
}

#
# Function to look someone up in the phone book
#

lu () {
        # put commands from lu program here
}

#
# Function to remove someone from the phone book
#

rem () {
        # put commands from rem program here
}

#
# rolo - rolodex program to look up, add, and
#        remove people from the phone book
#

# put commands from rolo here
$

None of the commands inside the original add, lu, rem, or rolo programs would have to be changed. These first three programs are turned into functions by including them inside rolo, sandwiched between the function header and the closing curly brace. Note that defining them as functions this way now makes them inaccessible as standalone commands.

Removing a Function Definition

To remove the definition of a function from the shell, you use the unset command with the –f option. This is the same command you use to remove the definition of a variable to the shell.

$ unset –f nu
$ nu
sh: nu: not found
$

The return Command

If you execute an exit command from inside a function, its effect is not only to terminate execution of the function but also of the shell program that called the function. If you instead want to just terminate execution of the function, you can use the return command, whose format is

return n

The value n is used as the return status of the function. If omitted, the status returned is that of the last command executed. This is also what gets returned if you don't execute a return at all in your function. The return status is in all other ways equivalent to the exit status: You can access its value through the shell variable $?, and you can also test it in if, while, and until commands.

The type Command

When you type in the name of a command to execute, it's frequently useful to know where that command is coming from. In other words, is the command actually defined as a function? Is it a shell program? Is it a shell built-in? Is it a standard Unix command? This is where the type command comes in handy. The type command takes one or more command names as its argument and tells you what it knows about it. Here are some examples:

$ nu () { who | wc -l; }
$ type pwd
pwd is a shell builtin
$ type troff
troff is /usr/bin/troff
$ type cat
cat is /bin/cat
$ type nu
nu is a function
$

Exercises

1:

Using eval, write a program called recho that prints its arguments in reverse order. So

recho one two three

should produce

three two one

Assume that more than nine arguments can be passed to the program.

2:

Modify the shar program presented in this chapter to handle directories. shar should recognize input files from different directories and should make sure that the directories are created if necessary when the archive is unpacked. Also allow shar to be used to archive an entire directory.

$ ls rolo
lu
add
rem
rolo
$ shar rolo/lu rolo/add rolo/rem > rolosubs.shar
$ shar rolo > rolo.shar

In the first case, shar was used to archive three files from the rolo directory. In the last case, shar was used to archive the entire rolo directory.

3:

Modify shar to include in the archive the character count for each file and commands to compare the count of each extracted file against the count of the original file. If a discrepancy occurs, an error should be noted, as in

add: expected 345 characters, extracted 343.


[1] Actually, what happens is that eval simply executes the command passed to it as arguments; so the shell processes the command line when passing the arguments to eval, and then once again when eval executes the command. The net result is that the command line is scanned twice by the shell.

[2] Some Unix systems use Ctrl+c rather than the Delete key for this purpose. You can determine which key sequence is used with the stty command.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.147.53