Você está na página 1de 102

Bourne_Shell_Programming-Robert_P_Sayle.

html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Bourne Shell Programming


By Robert P. Sayle

Author's Note
My younger sister asked me a question a few days ago that has forced me to reexamine my motivations for writing this book. She asked me, "What is a shell?" If I remember correctly, I did give her a five-and-dime answer, but I found that as I did so, I could not quite explain the entire concept satisfactorily. Since she posed this question, I have found the query creeping into my thoughts. It has challenged me to recall this text's origins, what my original intentions for it were, and what I now believe to be its scope and purpose. In doing so, I now understand that there is a fairly simple answer to her question, which I will get to in the first chapter, but I have also realized that there is a rich variety to what can be called a shell and a subsequent history of computing to which this book and the shell are linked. This book derives from a class I produced and taught during my programmer days at ARINC. At that time, I wanted to share with my colleagues how to customize UNIX rather than fight it. I was frustrated watching a group of talented C++ programmers fumble around in such a rich operating system. I figured I'd write a notebook that could explain the simple programming facilities available in the shell, and I would never again have to watch a friend hit the up-arrow key or enter !! just to monitor their program. So I took a couple weeks in between projects and wrote and presented what can now be considered the outline for this book. The only problem was that I had peaked my peers' interest in the shell. Soon I was receiving phone calls and office visits that required a more in depth explanation than what I had written on a few acetate slides. By this time, two very important trends in computing and networking started. A couple years earlier, a few smart chaps out at the University of Illinois had developed the idea of hypertext documents, and text is all they were, that would allow users to move through them by selecting words that linked documents across different networks. By the time I had given my class, the idea had bloomed into HTML and a little known buzz word called the World Wide Web. Seeing how fun and easy it was to create interlinked documents, with pictures and tables nonetheless, I thought it would be a great idea to try translating the class notes into an active book. Imagine how useful it would be to a reader to move from chapter to chapter at the click of a button or to follow a link from a concept presented in the book's index immediately to the written text describing it. Besides that, I figured HTML was a good format for its operating system independence as well as its growing acceptance among the Internet community. About the same time I started converting the class notes into HTML, the Microsoft juggernaut began gaining steam with a new operating system, Windows95. The key to this operating system was and still is its eye catching GUI. It was a big step over Windows 3.1. Suddenly, Microsoft had produced an easy to use and stable desktop operating system that anyone could use. Of course I could now discuss the merits of Macintosh and how much Win95 looks and feels like Macintosh, but I digress. Microsoft marketted the hell out of its new baby, and Windows95 sold like no other program or operating system. Being both a UNIX and Macintosh biggot, I would not succumb to the marketing hype. I promised myself. I would not do it. Linux or die! Then I saw that a good friend of mine had installed Win95, and people at work began using it. The trade magazines carried more and more articles talking about the use of Windows95 in corporate America. Slowly, it was closing in on me. I finally fell prey to Windows when a laptop I had ordered came preconfigured with it. I have to admit that once I used it I was hooked. Pretty soon, I noticed a majority of users inside and outside my office were using Windows95.

1 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

When Microsoft released Windows NT 4, which pretty much has the same interface as Win95, I knew it was over for UNIX. It would not happen overnight of course, but why bother writing a book that will be obsolete within a few years. So I shelved the book and actually made a career change that would move me away from daily programming into the design and implementation of data communication networks. While on this new path, I noticed the monthly toutings by various trade magazines of NT's market penetration. Charts and graphs that had been speculation of its deployment within a few years had become real. And why not? It is much easier for an administrator to point and click through an operating system then to have to learn a cryptic command line interface (CLI) like UNIX. Of course these same administrators also do not understand the flexibility and efficency of this CLI, but I digress again. Suffice it to say, NT had made its mark and was here to stay after a few years of introduction into corporate America. And then one day my sister asked me, "What is a shell?" I was surprised. She had been telling me about her new job as a database programmer and how the product ran on a couple variants of UNIX. She was telling me how she knew some UNIX from her previous job, but now she would really need to understand it, and then the question Since our discussion, I decided to take another look at NT and UNIX. I found reports of NT's shortcomings as a secure and stable platform. Serious problems with its networking and administrative modules could bring the operating system to an abrupt halt. Performace evaluations rated it poorly in, for example, its ability to act as a private gateway for remote access. I even had a couple customers tell me they would not use NT for mission critical applications. Instead, they were still deploying UNIX based machines. About all they were using NT for was for basic user accounting, file, and printing services. Still, I realize this is entirely a natural part of any operating system's life-cycle. NT will be an enterprise system someday, but as I write this book, it is not there. Hence, there is still a strong demand for UNIX. So here I sit, busily typing with a renewed vigor to teach my sister and anyone else who cares to learn how to program the Bourne shell in order to use UNIX to its fullest potential. Robert Sayle June 8, 1998

Acknowledgements
Thanks to Kathleen for inspiring me to revisit this text and complete it. Thanks to Mark for helping me clean up the interface. Thanks to Brian for dotting my i's and crossing my t's, so to speak.

Conventions
This document uses the following formatting conventions. Plain text is written in a normal style font: The Bourne shell contains many built-in commands. Commands and options entered by a user are displayed in a fixed width, bold font:
ls -lg /etc/raddb/users

2 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Responses and queries provided by the operating system are shown in a fixed width font:
/bin/ls: /etc/raddb/users file not found.

Table and diagram captions are given with an italic font: Table 1.2-1: Quoting Mechanisms

About the Book


Bourne Shell Programming teaches UNIX users how to harness the power of the shell. The book assumes that the reader has at least a general knowledge of UNIX including its commands, syntax, and operation. It also assumes that the reader also understands simple programming techniques inherent to most programming languages. The book does not provide instuction on the basics of UNIX. It instead builds upon these basics by showing how to combine them with the shell's programming facilities. The goal is to train users to employ these techniques at both the command line and within scripts so that the operating system becomes a tool instead of a hindrance. This book was written over the course of a number of years mainly because the author switched career paths. The change consequently resulted in a text tested against two different versions of UNIX. Some examples are shown in Sun Solaris 2.4 while others are given from Linux Slackware 3.2. Readers are cautioned to check their local operating system's manual pages on any command demonstrated for proper syntax and operation. The author realizes he is prone to error and respectfully requests any corrections be forwarded by email. Suggestions on improving this book are also welcome as are offers to publish it through traditional channels. For notes on the format of this book, the reader is directed to the conventions listed in the book's foreword.

1. Shell Basics
1. 2. 3. 4. 5. 6.

What is a Shell? Shell Types Grouping Commands I/O Regular Expressions Quoting

1.1 What is a Shell?


Simply stated, a shell provides an interface to the operating system facilities including files, printing, hardware devices, and applications. This is a very broad definition indeed. It applies from something as simple as a telnet interface to a complex windowing system such as the Common Desktop Environment (CDE). It also transcends operating system brands. For example, a ksh session on an HP UNIX workstation is as much a shell as a DOS window is to a Microsoft Windows 95 PC or a DEC windows session is to a VAX/VMS desktop server. In each case, there exist methods for reading files, queuing print jobs, or launching programs. On a Windows 95 PC, a user might read a file by invoking the Notepad program from
3 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

the Start Menu. An operator could use the PRINT command in VMS to view the current jobs queued for a line printer. A programmer might open a terminal window from the CDE control panel in order to begin compiling and linking a program. In each case, the interface gives the user some way to command the operating system. Another way to view a shell is to consider it in the context of a metaphor using its own name. Consider an egg shell. An egg shell is a hard, protective wrapper that encases a growing embryo. Needless to say, UNIX is hardly like a developing fetus, but the wrapper part fits. Figure 1.1-1 depicts the relationship. The UNIX shell can be treated as an operating system wrapper. It encapsulates the devices, applications, files, and communications that in turn access the kernel to perform all of the functions expected of computers. Once again, this is a simplistic explanation of how a user interoperates with a computing system, but it should help demystify the concept of a shell.

Figure 1.1-1. A Shell as an Operating System Wrapper

1.2 Shell Types


Naturally, this book deals with a very specific shell, namely, the UNIX Bourne shell. There are a number of similar shells that are worth mentioning, but first, it is important to note that each of these shells shares the following qualities: Command line interface (CLI), Rigid syntax, High degree of flexibility, Programmable. The traditional UNIX shells are command line driven. A user enters textual input consisting of operating system commands with options and switches. The computer in turn provides textual output according to the command given followed by a prompt for the next command. The richness of an application's options combined with the built-in interpreted programming language of the shell allows a user to easily command the operating system to perform very specific functions. In a graphical environment, this same flexibility is lost in a program whenever a user is forced to use check boxes, radio buttons, or selection lists. It is much more expedient to include all the options on one command line then it is to point-and-click through a pop-up window. On the other hand, this same flexibility loses the ease of use that GUIs provide. A GUI can very easily force a user to review all necessary options before running a program. A command line driven system requires the user to research the command thoroughly before issuing it. Still, the syntax maybe restrictive, but the fact that these same commands can be easily stored in an executable file and replayed at a later date once again point

4 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

to the shell's customizability. Moreover, the branching and looping facilities of the shell allow it to make programmed choices and to repeat commands as necessary. This is an invaluable quality lost in the point-and-click world of a windowing system. As mentioned previously, there are a number of shells to choose from, but it must be noted that there are only two basic types. The first is the Bourne shell. It has a unique syntax different enough from traditional programming languages to require concerted study by those willing to learn it, but it is does have some very familiar constructs that also keep it from being an entirely foreign language. The second type is based upon the C shell. It is named the C shell because its syntax derives from the C programming language. It is in fact similar enough syntactically to allow C programmers to quickly use the branching, looping, and assignment mechanisms. One might easily be able to write if statements and while loops, but the environment is different enough to cause problems if not treated carefully. A user might easily mistake a string for a variable dereference. From these two shell types came a number of highly-interactive shells. The Korn shell, ksh, and the Bourne Again shell, bash, use Bourne shell syntax. In general, any command useable by the Bourne shell may be used by these others. The C shell's cousin is the tcsh. These derivatives are more user-friendly compared to their parents. They usually provide hot-keys for utilities like file name completion, directory searching, in-line editing, and an extended environment. In some cases, such as the Korn shell, they also allow complex data structures such as arrays. These shells are definitely preferrable for daily use. It is much nicer to hit the tab key and have the shell complete a directory name than it is to have to type the whole thing, especially when the exact name is unclear. And the beautiful part is when something like a loop is needed to allow a command to be repeated, the facilities are right there. Plus, there is often a history buffer that caches recently issued commands so the user does not have to reenter or restructure it. But it is the basics that are explained herein. This book presents the syntax and use of the Bourne shell. It does so for a very good reason; namely, it is the most widely available of any shell. It is the first shell that came with UNIX. Consequently, it has survived and is distributed with every flavor of UNIX produced today. It can be found in every UNIX as /bin/sh. Because of its availability, any program written that uses it is nearly guaranteed to run on any other UNIX. Of course portability carries only as far as the actual operating system commands allow, but at least the basic programming tools and syntax of the Bourne shell are consistent. From this point forward, any mention of the shell refers to the Bourne shell unless explicitly stated otherwise. Before diving right into the usual programming bag o'tricks such as variables, functions, if statements, loops, and other fun stuff, this chapter will conclude with some very important syntatical features. Without a thorough understanding of them, it is extremely difficult to read and write shell scripts. For those unfamiliar with the shell or UNIX in general, return to this chapter occassionally and reconsider them in the context of commands given at the prompt or within other shell scripts encountered.

1.3 Grouping Commands


Commands passed to the shell may be grouped in five ways with a semicolon (;), parentheses (()), curly braces ({}), double ampersands (&&), or double vertical bars (||). The first three may be simply viewed as punctuation for combining multiple commands on a single line. Of course there are subtle differences between them that will be discussed shortly, but in general, they are to the shell as periods are to prose. The last two, the double ampersand and vertical bars, are conditional conjunctives. The semicolon allows users to string commands together on a single line as if one command was being issued. Each command within the semicolon separated list is executed by the shell in succession. For example, the line below has two commands joined with a semicolon:
$ cd /var/yp; make

When entered, the shell parses the command line up to the first semicolon. The words read to that point are executed. In the example, the current directory is changed to the /var/yp directory. The shell then continues

5 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

parsing the command line until it reaches the end of the line. The rest of the command is then executed, and as every good UNIX administrator knows, the NIS maps are built as a result of the make that is issued. But this example is not a lesson in system administration. It is a demonstration of how the shell executes each semicolon separated command in the order it is listed after the prompt as if the user had typed the same set of commands on separate lines. The parentheses, when grouped around a command, cause the command to be executed in a subshell. Any environmental changes occuring within the subshell do not persist beyond its execution; in other words, all changes to the working environment of the commands issued within the parentheses do not affect the working shell. Consider the following example:
$ cd; ls bin etc include man tmp $ cd bin; ls; ls bash gunzip gzip zcat bash gunzip gzip zcat $ cd $ (cd bin; ls); ls bash gunzip gzip zcat bin etc include man tmp

The first command ensures that the home directory is the current working directory and then lists its contents. This is followed by changing directories into bin and then listing its contents twice with the ls command. The next command then returns the shell to the home directory. The last command shows how parentheses behave. Two commands are issued within the scope of the working shell. The first is a compound command delimited by the parentheses. The compound command executes in a subshell. Within this subshell, the current directory is once again set to bin, and its contents are listed. As soon as the subshell terminates, the parent shell resumes control. It lists the contents of the working directory, and as is demonstrated by the resulting output, the shell's current working directory has not been modified by the subshell's directory change. Like parentheses, curly braces can be used to group a set of commands; however, the commands execute within the context of the current shell. Continuing with the previous example, suppose the last command is reissued with curly braces instead of parentheses.
$ { cd bin; ls; }; ls bash gunzip gzip zcat bash gunzip gzip zcat

Notice that this time the bin directory is listed twice. Obviously, the directory change persisted. The shell executed the compound command instead of deferring it to a subshell as it did when parentheses were used. Note also the construction of commands with culry braces. White space precedes and follows the enclosed commands, and a semicolon terminates the last command. The terminating semicolon is required. Certainly, the use of curly braces can be unwieldy:
$ { cd bin; ls }; ls > ^C $ { cd bin; ls }; ls } > ^C $ { cd bin; ls }; ls; } }: No such file or directory bash gunzip gzip zcat

The first command is entered the same way it would be issued if parentheses were used. When entered, the shell responds with the secondary prompt meaning that it expects more input. The job is killed with a control-c, and the command is reentered with a different format. Here, an extra closing brace is appended to the command. The user assumes that the shell was expecting a terminating brace at the end of the line. Unfortunately, the results are the same. After killing the second attempt, the user tries a third time and appends a semicolon to the final ls command. As can be seen, the command becomes syntactically correct, but the results are probably not what the user desires. Focusing on the semicolons reveals what happened. The shell changes directories to bin. Then it tries to list the } directory. The shell interprets the closing brace to be an argument to the ls

6 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

command. Since no such file exists, it complains. It finishes by listing the current directory. Hopefully, from this example, the point is clear. Curly braces are, frankly, difficult to use for grouping commands, and when also using culry braces for accessing variable values, code becomes very difficult to follow. It is generally good practice not to use curly braces for command grouping. Use culry braces for variables instead. The last two conjunctions, && and ||, combine commands conditionally. When the shell encounters either of these, it always executes the first half of the command. Then depending upon its results, the second half may be run. In the case of the double ampersand, the shell handles the second part if and only if the first part exited with a status of zero. This indicates that the first half executed with no errors.
$ cd /usr/bogus && ls /usr/bogus: does not exist $ cd /var/spool && ls calendar cron locks lp mqueue pkg

Since /usr/bogus is not a valid directory, the cd command informs the user and returns with a non-zero status. The shell reads the status and, seeing it is non-zero, does not execute a directory listing. On the other hand, /var/spool is a real directory. The cd command, in this case, performs the change and returns with a zero exit status. Again, the shell reads the status, and because it is zero, the shell proceeds with the following command, the ls. The double vertical bars function the same except that the second half executes if and only if the first half exits with a non-zero status.
$ ls -l total 4 drwxr-xr-x 2 rsayle fvsw 512 Jul 15 drwxr-xr-x 2 rsayle fvsw 512 Jul 15 -rw-r--r-- 1 rsayle fvsw 0 Jul 15 $ (ls -l | grep "root") || echo "No No files owned by root

14:51 14:51 14:51 files

bin man profile owned by root"

The first listing above shows that all files in the current directory are owned by rsayle. Hence, the grep for root fails. A non-zero exit code is returned by the subshell that executed the ls and the grep. The parent shell, having encountered ||, acknowledges the result and prints the informational message accordingly.

1.4 I/O
The UNIX Bourne shell contains a couple methods for manipulating the input and output of commands. The first method employs pipes. It can be used to chain commands together so that each preceding command sends its output to the input of the following command. The second method is known as redirection, and it provides the ability to pull input from or place output into other sources besides the shell window. But before introducing these commands, a general understanding of the standard I/O facilities must be reached. The input and output displayed on the console comes from three buffers: stdin, stdout, and stderr (pronounced "standard in," "standard out," and "standard error," respectively). These devices are collectively referred to as the standard I/O. Stdin is the terminal's default input. The shell reads commands and answers to prompts from stdin. The default output is stdout. The console prints anything it receives from stdout. In general, this includes print statements from the programs run at the command line and warning messages broadcast by system administrators. Like stdout, the shell also displays the error messages received from stderr on the console, but it is imperative to keep in mind that stderr is not the same as stdout. A good way to remember this is to think of stdin, stdout, and stderr as three different files. In fact, this is exactly what they are. Stdin is file descriptor zero; stdout, one; and stderr, two. As such, each may be referenced by its file descriptor number. Section 8.1, Advanced Shell Topics: More I/O, shows how to manipulate the standard I/O using the file descriptor numbers. A pipe, represented by a single vertical bar (|), connects the output of its left operand to the input of its right

7 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

operand. The operands of pipes may be any legal shell command. The goal of piping is to pass output through a series of commands that will massage the data into a more meaningful representation. Normally, the shell parses stdin and passes the input to the command given. The command operates on the input, and in turn, presents the output to stdout. The shell handles pipes as is shown in the figure below. Here, the shell begins as it always does. It parses stdin passing arguments to the command issued. The program runs and provides output. Seeing a pipe, the shell takes the resulting output and passes it as the input to the next command. The next command works on the new input and creates a new ouput which is passed on to the following command in the pipe chain. This continues until the shell reaches the final command. The last command receives the massaged data from the previous command, runs, and then places the result into stdout. The shell the displays stdout to the user.

Figure 1.4-1. A Graphical Depiction of How Pipes Combine Multiple Commands The most common use of pipes is for filtering. Typically when filtering, a program prints text to stdout which is piped into a utility such as grep, sed, or awk. The utility selects data from the input according to a pattern specified as an argument. The result is usually posted to the screen for visual inspection, but it is possible to save the results in a file, store it as the value of a variable, or repipe it into another command.
$ ypcat passwd | grep "^[a-zA-Z0-9]*::" | cut -f1 -d: jbeadle build35 cwoodrum dborwin jbell dfellbaum

The example above shows how grep and pipes can be used as a filter. The command is actually three programs separated by pipes. The first command, ypcat passwd, displays the network information services (NIS) map for the password database. The shell, as directed by the pipe, sends the map to grep. Grep takes the map and searches for all accounts that do not have a password; this is the filtering operation. Since the goal is to list the accounts, some further processing must be completed so the output is passed to cut. Cut collects the data and trims everything from each line except the account name. The final output is displayed as shown. The figure below depicts the example graphically.

Figure 1.4-2. A Graphical Depiction of a Pipe Chain Whereas piping permits the chaining of commands such that the output of one serves as the input of another, redirection can be used to take the input for a program from a file or to place the output of a program into a file. Not surprisingly, these two techniques are known as input redirection and output redirection. Input redirection takes the form: command [input redirect symbol] input. It is equivalent to saying, "Execute a command using the data that follows." Redirecting the output says, "Execute a command and place the printed results into this file," and it has the form: command [output redirect symbol] destination.
$ cat < /etc/defaultdomain fvo.arinc.com $ ypcat hosts > hosts.bak $ cat hosts.bak 127.0.0.1 localhost 144.243.92.13 blatz

The previous commands demonstrate input and output redirection. The first cat command takes as its argument the file /etc/defaultdomain. The argument is given to cat via input redirection, the left brace.

8 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Output redirection is shown with ypcat. The ypcat command, when applied to the NIS hosts map, lists all the IP addresses and names of each node in the NIS domain. Here, the output is redirected into the file hosts.bak. Two types of input redirection exist. The first one uses one left brace. It redirects standard input to be taken from the file specified. For example, if an administrator wanted to know how many accounts were on a particular machine, input redirection of the /etc/passwd file into wc could be used.
$ wc -l < /etc/passwd 12

The second type employs a double left brace. Instead of taking the input from a file, the input is taken at the command line between two delimiters.
$ wall << WARNING > "itchy will reboot at 6PM this evening" > WARNING Broadcast Message from rsayle (pts/0) on itchy Wed Jul 24 14:06:57... "itchy will reboot at 6PM this evening"

The delimiters may be any word. In this case the word " WARNING" is used. The user here issues the warn all users command, wall. The user also instructs the shell to use as the text of the message the lines that fall between the two WARNING delimiters. Similarly, there are two types of output redirection. The symbols used are simply inverses of the input redirectors; right braces are used instead of left braces. And of course, the semantics are nearly opposite. In the case of the single right braces, stdout is redireced to a file. The file is created if it does not exist or it is overwritten if the file named already exists and globbing has been enabled ( set glob). Below, the shell copies the file combo to the file combo.bak using output redirection. The output from the second cat command is redirected into the new file combo.bak.
$ cat 10 23 $ cat $ cat 10 23 combo 5 combo > combo.bak combo.bak 5

Double right braces have the same effect as single braces except if the file already exists, the output is appended to the file instead of overwriting it.
$ cat $ cat 10 23 10 23 combo >> combo.bak combo.bak 5 5

Continuing with the previous example, if the user instructs the shell to once again display the contents of the file combo but uses double right braces, then combo.bak ends up with two lines, each being the single line from combo. As a final I/O basics topic, it is helpful to mention that there are a few generic devices available for redirection besides the standard I/O. Some flavors of UNIX provide devices for the terminal, /dev/tty, the console, /dev/console, and the great bit bucket in the sky, /dev/null. All output sent to /dev/null is simply discarded. It is useful in shell scripts when output should be hidden from the user but should not be stored. There is a subtle difference between the console and the terminal. In a windowed environment, a tty terminal is associated with each shell session; in other words, each command line window such as an xterm is a separate terminal. Redirection to /dev/tty sends output to the active window. The console, on the other hand, is the screen. It is the monitor in general. Most windowing programs provide a special switch to the shell windows that will link /dev/console to the window instead of writing output directly on the screen. In any event, output can be redirected to these devices quite easily.
$ cat /etc/motd >>/dev/tty Sun Microsystems Inc. SunOS 5.4 Generic July 1994

9 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ cat /etc/motd >>/dev/null $

1.5 Regular Expressions


Regular expressions are one of the keys to UNIX. To master them is to open the door to a new universe of commands. A user who has a firm grasp of regular expressions can perform multiple tasks with one command, filter text for processing, and edit multiple files nearly instantaneously. And these are but a few examples of the power regular expressions provide. A regular expression is a sequence of characters specifying a textual pattern. They instruct programs to process only those items that match the pattern. Most regular expressions use metacharacters to express repetition, existence, or ranges of character patterns. It can be very difficult at first to decipher a regular expression that uses metacharacters. Nevertheless, it is extremely important to practice creating and reading regular expressions. It is truly the difference between writing robust scripts and simply browsing around through UNIX.
^$ [a-z][0-1][a-z0-1]* ^<tab>[A-Z][a-zA-Z0-9 ]*$ 10\{3,6\}

At first glance, to the novice user, the examples above look like gibberish, but a patient and disciplined user can understand what each pattern means. In the first line, the carat represents the beginning of a line and the dollar sign the end of a line. Together, with nothing in between, they form a pattern describing an empty line. The second example shows how character classes can be used in regular expressions. The pattern represents a line that contains a word beginning with a lower case letter, followed by a zero or one and ending with any number of lower case letters, zero, or one. The square braces denote character classes, and the asterisk is a wildcard meaning any number including zero of the previous pattern. The third example is even more restrictive than the second. It matches patterns beginning with a tab (the tab normally is just typed; the <tab> is shown here for illustrative purposes), followed by an upper case letter, followed by any number of lower case, upper case, digit, or space character, and terminated by the end of the line. It is certainly a complex example, but it shows how by examining the pattern piece by piece, the meaning becomes clear. The last expression matches the number range 103 through 106, inclusive. It specifies a pattern beginning with ten followed by any number in the range three through six. A list of the metacharacters and their usage is given in the table below. The five metacharacters at the end are part of an extended set. They only have meaning in certain programs.

10 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Table 1.5-1. Metacharacters Metacharacter


.

Usage Matches any single character except newline.

Used by All utilities

Matches zero or more occurrences of the single character that immediately pr All ecedes it. The character may be specified by a regular expression. utilities Matches any one of the class of characters enclosed between the brackets. I f a caret (^) is the first character inside the brackets, then the match is reversed. A hyphen is used to specify a range. In order to match a cl ose bracket, it must All utilities be the first character in the list. Other metacharacters l ose their special meaning when enclosed within brackets so that they may be matc hed literally. As the first character of a regular expression, matches the beginning of a l ine. As the last character of a regular expression, matches the end of a line. Escapes the metacharater which immediately follows. Matches a range of occurrences of the single character that immediately prec edes the expression. If only m is given, then the pattern matches exactly m repetitions. If m, is specified then at lea st m occurrences are needed for a match. The use of m,n matches any number of repetitions between m
and n.

[]

All utilities All utilities All utilities sed, grep, egrep, awk egrep, awk egrep, awk egrep, awk egrep, awk

/{m,n/}

Matches one or more of the preceding regular expression. Matches zero or one instance of the preceding regular expression. Matches either the preceding or following regular expression. Groups regular expressions.

()

11 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

There are a number of utilities available that use regular expressions. In particular, it is useful to study the one built into the Bourne shell, filename completion. Filename completion assists in locating files or lists of files. It uses its own set of metacharacters: *, matches zero or more chracters; ?, matches any single character; [], matches any single character listed (a ! reverses the match); and \, removes the meaning of any special character including and especially the end of a line. * is typically called the wildcard, and \ is referred to as escape. Filename completion works by causing the shell to expand the pattern into all file names that match the expression.
$ ls as*.* ascendmax.alert ascendmax.emerg ascendmax.info ascendmax.warning ascendmax.crit ascendmax.err ascendmax.notice $ cd .. $ ls log/as*.* log/ascendmax.alert log/ascendmax.err log/ascendmax.warning log/ascendmax.crit log/ascendmax.info log/ascendmax.emerg log/ascendmax.notice

As part of its parsing routine, the shell expands regular expressions before executing the command listed. The expansion acts as if the user had typed a list of files at the command line. In the preceding example, the shell expands the pattern as*.* into all files in the current directory beginning with as and having a period somewhere in the middle. The result is substitued for the pattern and the command is issued to list the files. The second part shows how the shell can be directed to traverse directory trees and perform expansion on subdirectories. Some of the other common programs that use regular expressions are given in the table below. Table 1.5-2. Programs that Use Regular Expressions Utility
grep sed

Purpose Prints each line from its input that matches the pattern specified.

Reference for Further Reading man

Permits line by line editing of its input according to the script of regular man, Sed & Awk expressions and commands given. (O'Reilly & Associates) Programming language for extracting and formatting input according to man, Sed & Awk textua l patterns. Works best when the input already has a particular (O'Reilly & Associates) structure. Extracts text from the input given a list of field numbers and their field s man eparator. Filters out repeated lines from its input so that only unique lines remain i man n the output. Sorts the lines of input given. man

awk

cut

uniq sort

The most common forms of input to these utilities are files or pipes:
$ grep "defs.h" *.cpp | cut -f1 -d: | uniq bench.cpp bitvec.cpp bstream.cpp gimp.cpp idendict.cpp match.cpp regexp.cpp rwbag.cpp rwbagit.cpp rwfile.cpp rwtimeio.cpp strngcv.cpp toolmisc.cpp utility.cpp wcsutil.cpp wstring.cpp

This command uses files and pipes. The user wants to list all C++ source files that include the header file
12 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

The command begins by searching for the string defs.h in all files ending with the extension .cpp. The files found are passed to cut. Normally, grep prints the file name and every line in a file that contains the pattern. Here, cut is used to trim the lines down to the file name only. The result is in turn passed to uniq which filters out duplicated lines.

defs.h.

1.6 Quoting
Just as regular expressions separate UNIX wizards from users, the use of quoting differentiates shell scripters from shell hackers. Quoting is simple in form and powerful in semantics. With quoting users can make metacharacters act like regular text or nest commands within others. A long command can be easily separated onto multiple lines by using quotes. But with this powerful tool comes a price. Proper usage of quoting is at best subtle. Often it is the source of program error and is difficult to debug. This should not deter scripters from quotes. Instead, users must simply realize that quoting takes practice. Bourne shell quotes come in four forms: 1. Double Quotes "" Removes the special meaning of all enclosed characters except for $, `, and \. 2. Single Quotes '' Removes the special meaning of all enclosed characters. 3. Back Quotes `` Command substitution; instructs the shell to execute the command given between the quotes and substitute the resultant output. 4. Escape \ Removes the special meaning of the character that follows. Inside double quotes, \ also escapes $, ', newline, and escape itself. The primary use of double and single quotes is to escape the meaning of metacharacters within blocks of text:
$ echo * bin man profile $ echo "*" * $ echo $USER rsayle $ echo "$USER" rsayle $ echo '$USER' $USER

The first two commands show the difference between using quotes. Special characters are interpreted by the shell when they are not quoted as in the case where the echo command displays the contents of the current directory: bin, man, and profile. The same can be said of the third command in which the value of the environment variable USER is echoed. Moreover, it is important to remember that there are a few special characters that are still interpreted when used with double quotes. Single quotes must be used to perform the escape as is shown by the last few commands. Double quotes and single quotes are especially useful for patterns containing white space. Suppose, for example, an administrator knows a person's name but can't remember the user's account name. Filtering the passwd database through grep might be a good way to figure out the account name.

13 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ ypcat passwd | grep Robert Sayle grep: can't open Sayle Grep

balked on the user's name! Well, not exactly. Due to its syntax, grep tried to find the string "Robert" in the file "Sayle." The trick is to instruct the shell to ignore the white space between the first and last name.

$ ypcat passwd | grep "Robert Sayle" rsayle:hOWajBzikAWRo:328:208:Robert Sayle:/home/rsayle:/bin/tcsh

The double quotes tell the shell interpreter to combine all the text contained into one argument, " Robert Sayle." Grep receives the argument and uses it as the key during its search through the output of the ypcat. To search for quotes within text, single quotes escape double quotes and vice-versa. The following examples illustrate this. The first and third show how one cannot simply use a quotation mark in pattern matching. The shell assumes that more text follows the input. The other examples show how to do it correctly.
$ > $ # grep " sloc ^C grep '"' sloc a given set of files; finds each ";" which denotes an echo "sloc: usage: sloc file1 [file2 ...]" LINES=`grep ";" ${1} | wc -l` echo "${RESULT} lines of code" $ grep ' tgzit > ^C $ grep "'" tgzit # tgzit -- tar's and gzip's the filenames passed

Although these are simple examples, not enough can be said about the difference between single and double quotes. Double quotes function perfectly in most cases, but there are times when a programmer should use one over another. The realm of possibilities will be explored in succeeding subsections as new topics are presented. For now, a good rule of thumb is to use double quotes in order to group words into a single argument, to allow variable substitution as in "$USER", or to allow command substitution. Single quotes should be used when no substitution should occur. With that said, the discussion of quotes in general continues. Back quotes are arguably the most powerful form of quoting since they permit a level of nesting called command substitution. In other words, they allow commands to be executed within other commands. When the shell scans a command line and finds an embedded command between back quotes, it runs the nested command. The output of the nested command is then substituted as part of the enclosing command which is subsequently executed. As an example, suppose a programming team places their source files in a common repository.
$ ls -l *.h -r--r--r--r--r--r--r--r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--r--r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--r--r--r-1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 rsayle rsayle rsayle lshar lshar lshar lshar lshar lshar rsayle rsayle lshar rsayle lchang 346 346 rsayle lshar rsayle 346 lshar lshar lshar rsayle fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw 5373 5623 4930 20264 3346 6819 3424 7446 3394 35012 4959 4851 5024 9106 8627 9454 5025 6260 4871 9512 17087 14056 3268 5263 Aug Aug Aug Aug Aug Aug Aug Aug Aug Aug Jul Aug Jul Jul Jul Jul Aug Jun Jul Jul Aug Aug Aug Aug 3 7 10 14 14 14 14 14 14 16 25 17 25 19 25 25 3 19 26 25 14 14 14 2 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 adminlink.h agentadmin.h agentadminexception.h attribute.h attributeexception.h attributehelper.h attributehelperexception.h attributereference.h attributerefexception.h attributevalue.h avdictionary.h avexception.h avtype.h computeropstatemsg.h elevation.h latitude.h linkagentadmin.h linkfactory.h linkmibloader.h longitude.h managedobject.h mib.h mibexception.h mibloader.h

14 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

-r--r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--r--r--r--rw-r--r--rw-r--r--r--r--r--rw-r--r--rw-rw-rw-rw-rw-rw-rw-r--r--r--r--r--r--r--r--r--r--r--rw-r--r--r--r--r--r--r--r--

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

rsayle lshar lshar lshar lshar lshar lchang lchang lchang lchang rsayle rsayle 346 rsayle rsayle lshar lchang lchang 346 tbass rsayle rsayle lshar rsayle rsayle

fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw fvsw

4910 3255 8101 3346 6134 3424 5008 3232 7365 3215 4823 3815 9342 10085 4790 4174 3207 9503 10514 2423 2785 5210 7909 5544 10938

Aug Aug Aug Aug Aug Aug Aug Aug Aug Aug Aug Aug Jul Aug Aug Aug Aug Aug Jul Jul Jul Aug Aug Aug Aug

10 14 23 14 14 14 7 7 7 7 10 10 25 7 10 14 17 17 25 12 26 7 17 2 24

1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995

mibloaderexception.h moexception.h mofactory.h mofactoryexception.h mofactoryhelper.h mofactoryhelperexception.h msgsvccbhandler.h msgsvccbhdlrexcep.h msgsvchandler.h msgsvchdlrexcep.h newavexception.h nmmsgids.h operationalstate.h opstatemsg.h ovsexception.h parserhelper.h regidexception.h regidserver.h rollablecounter.h servicedefs.h startup.h svagentadmin.h svfactory.h svmibloader.h svovshandler.h

The ls command output can be simply piped to grep with the account name, but in order to generalize the script, command substitution could be used.
$ whoami rsayle $ ls -l *.h | grep `whoami` -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -rw-r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -rw-r--r-1 rsayle fvsw -rw-r--r-1 rsayle fvsw -rw-r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -rw-r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -rw-r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw -r--r--r-1 rsayle fvsw

5373 5623 4930 35012 4959 5024 5025 4871 5263 4910 4823 3815 10085 4790 2785 5210 5544 10938

Aug Aug Aug Aug Jul Jul Aug Jul Aug Aug Aug Aug Aug Aug Jul Aug Aug Aug

3 7 10 16 25 25 3 26 2 10 10 10 7 10 26 7 2 24

1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995 1995

adminlink.h agentadmin.h agentadminexception.h attributevalue.h avdictionary.h avtype.h linkagentadmin.h linkmibloader.h mibloader.h mibloaderexception.h newavexception.h nmmsgids.h opstatemsg.h ovsexception.h startup.h svagentadmin.h svmibloader.h svovshandler.h

The whoami command returns the current account name. When used in the script, the shell executes whoami and substitutes the result for the argument to grep. Hence, anyone using the script would get results specific to their account name. Finally, the last quoting mechanism is the escape represented by a backslash character, \. It removes the special meaning of the character immediately following. The most common use is for line continuation:
$ /usr/bin/dump 0usbdf 6000 126 54000 /dev/nrst0 \ > /dev/sd0h >> backup.log 2>&1

The long backup command above could not fit within one line so an escape was entered before the end of the line. It escaped the newline causing the shell to wait for more input before parsing the command. Actually, a backslash escapes the special meaning of any character immediately following it. Returning to the first example in which single and double quotes were used to escape metacharacters, a back slash can provide some similar and some not so familiar results.
$ echo *; echo "*"; echo \* bin man profile *

15 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

* $ echo $USER; echo "$USER"; echo '$USER'; echo \$USER; echo \$$USER rsayle rsayle $USER $USER $rsayle $ echo hello; echo "hello"; echo hell\o hello hello hello

As can be seen, the escape functions most like single quotes, but it is imperative to keep in mind that it does so for exactly one character following the back slash. The last two echo permutations of the USER variable demonstrate this fact. As an aside, the last command string shows how escapes can precede characters that have no hidden meaning to the shell. There is no effect. For the final example, double quotes and escapes are used to set a variable's value for which the directory list is too long to fit upon one line:
$ DIRS2ARCHIVE=" \ > $HOME/tmp/bin \ > $HOME/tmp/man \ > $HOME/tmp/profile \ > " $ echo $DIRS2ARCHIVE /home/rsayle/tmp/bin /home/rsayle/tmp/man /home/rsayle/tmp/profile

2. Introduction to Scripting
1. Customizing UNIX with Shell Scripts 2. A Simple Example

2.1 Customizing UNIX with Shell Scripts


Why the heck would anyone want to write shell scripts? After all, a script is just a bunch of commands that could be issued from the shell prompt anyway. Well, sure. But how many times does a user have to reenter the same command before his fingers become knotted and arthritic? Wouldn't it be easier to store those commands in a file that can be executed by the computer? Certainly the CPU can execute commands faster than a human can type. Besides, isn't it smarter to modularize tasks so they can be combined and reused in an efficient manner? The fact of the matter is that shell scripts can provide this and more. Shell scripts can help organize repeated commands. An administrator who often repeats a series of operations can record the command set into a file. The administrator can then instruct the shell to read the file and execute the commands within it. Thus, the task has been automated; one command now does the work of tens, hundreds, even thousands of others. Shell scripting helps users handle the operating system smarter. The techniques presented in this discussion can be applied at the command line prompt. They are, after all, commands themselves; albeit, they are built into the Bourne shell. Still, by developing a habit of scripting at the command line, users can save time and effort by applying a script to a task rather than executing it by brute force. A good example is processing a set of files with the same command. A user can type the same command with a different file name over and over, or the user may write a loop that iterates over the files and automatically generates the same command set.

16 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Furthermore, shell scripting is simpler than conventional programming. Unlike compiled languages such as C and C++, scripts are interpreted. There is no compiling statements into machine code and linking it with libraries to form an executable. The script is the executable. Commands entered into it are immediately ready to run. On the other hand, scripts run slower than compiled programs and are often harder to debug mainly because no debugging tools are available. Even so, shell scripts are easier to develop and modify. Hopefully, the point is clear. Scripting is powerful, simple, and fast. Anything that someone can think of entering at the command line can be automated by a shell script: Reformat a file set so that the only white space in every file are spaces. Translate a file into another format for printing. Startup and shutdown a system. Extract and insert information from and into a database. Clean up a UNIX account. Collect and present system performance measurements. Monitor a remote workstation or accounts. Prepend a template prologue to source code for documentation. And all the other thinks you can think!

2.2 A Simple Example


At the risk of sounding cliche, a good shell script reads like a well written story. All right, maybe not a story, but it does take a form similar to a classical essay. It has an introduction, a body, and a conclusion. The introduction prepares the script much like the reader of an essay is prepared for the paper's contents by its introduction. A script's body presents commands to the shell just as an author presents an argument through an essay's body. And, in both cases, the conclusion wraps up loose ends. The shell script listed below illustrates this point. It is read from top to bottom with each line encountered being executed in sequence. Normally, the text is stored within a file that has execute permissions set. A user simply enters the file's name at the command line prompt, and the program is run. For illustrative purposes, the script is shown.
#!/bin/sh # cleandir -- remove all the large, unused files from the current directory echo "Removing all object files..." rm *.o # all object files have a .o extension echo "done." echo "Removing core dumps..." rm core # there's only ever one core echo "done." exit 0

Before analyzing the script, a quick note about comments is deserved. Comments are text that is ignored by the interpreter. Programmers provide comments to describe their code to readers. Comments begin with a hash mark (#). They may start anywhere within a line, and the comment always extends from the hash mark to the end of the line. A comment spans one line only; multiple line comments are not allowed. The script above shows the two types of commenting. The file prologue comment extends across a whole line:
# cleandir -- remove all the large, unused files from the current directory

On the other hand, the comments following the remove statements are inlined. They share the same line with the command:
rm *.o ... rm core # all object files have a .o extension # there's only ever one core

The first line of the script is not a comment. Instead, it specifies which shell shall be used to process the succeeding commands. Considering the comparisson of a script to an essay, it can be said that the first line

17 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

introduces the script's shell. It is the introduction. The basic syntax for declaring the shell has the form: #![full path to shell program]. The exclamation point following the hash mark signals the interpreter that the line is not a comment. It tells the interpreter that the text that follows is the path to a program in which to execute the script. In the example above, the Bourne shell is used, /bin/sh. It is not mandatory for a script to specify what shell program to use. If no such specification is given, then by default, the script is executed in a subshell of the calling shell. In other words, the script uses the same shell as the invoking shell. This is potentially dangerous. Suppose a user operates from a different shell than the Bourne shell. The tcsh is a good example. It has a much more user-friendly command line interface than sh, and it uses the same syntax as the csh. This syntax looks like C source code. It is very different from sh syntax. Suppose also that the user writes a script with Bourne shell syntax but neglects to make #!/bin/sh the first line. When the user invokes the script from the tcsh prompt, the interpreter reads the script and determines what shell to use. Since no shell is specified, it invokes a subshell of the tcsh, and executes the script. Chances are the program fails miserably. Section 9, Debugging Hints contains an example that demonstrates such a failure. A shell script's body is the list of commands following the shell declaration. It tells the program's story: what is it doing and how is it doing it. The shell processes the command body sequentially with branching, looping, and function constructs being exceptions to the rule. Any command that can be issued from a shell prompt may be included in a script's body. The example's body is the set of echo and rm statements:
echo "Removing all object files..." rm *.o # all object files have a .o extension echo "done." echo "Removing core dumps..." rm core # there's only ever one core echo "done."

This script tells the story of cleaning up a directory. It begins by informing the invoker with the echo that it will remove the object files. The next line actually performs this action with the rm on all files containing a .o extenstion in their name. This command in turn is followed by two more echo commands. The first indicates that the previous action completed. The second informs the user what it will do next. A second remove follows the two echo statements. It deletes any local core dumps. After completing, control passes onto the last echo which informs that user that the core dump removal finished. By default, a script concludes as soon as it has reached the last statement in its body. At this point, it returns control to its invoker. It also returns an exit status code. The exits code of a script is the return status of the last command run by the script. It is possible, however, to control this value by using the exit command. It takes the form exit status where status can be any non-negative integer. Zero usually indicates no errors were encountered by the script. Non-zero values indicate detection of various faults defined by the script. It is up to the programmer to determine what these faults are and to assign exit codes to them. The example terminates gracefully hen the body has been executed. It signals success by returning a zero status code:
... echo "done." exit 0

It must also be noted that unlike an essay, a script can provide a conclusion anywhere within its body. To terminate the execution of a script prior to reaching its end, the exit command must be used. This is generally done when a program detects an unrecoverable error such as insufficient parameterization. Once again, scripters are responsible for determining errant conditions and deciding whether to terminate the program or not.

18 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

3. Variables
1. 2. 3. 4. 5. 6.

Shell Variables Defined Declaring Variables Properties of Variables Environment Variables Special Parameters Reading Data into Variables

3.1 Shell Variables Defined


Just like any other programming language, the shell supports variables. A variable is a logical name for a stored value. The value may be changed during program execution, hence, the term variable. In the Bourne shell, a variable's name may be any string of alphanumeric characters and underscores ( _). Whitespace is not allowed. Examples of legal variable names include:
COUNT HOME_DIRECTORY _file1

Illegal variable names include:


FILE* The Answer array(i)

Some variables are predefined by the shell. They are known as environment variables and special parameters. All other variables are user defined. It is the scripter's responsibility to declare such variables and assign them values.

3.2 Declaring Variables


A script assigns a value to a variable by declaring the variable's name immediately followed by an equal sign (=) and the value.
COUNT=1 HOME_DIRECTORY=/export/home/bugsy _file1=table.dat

No white space is permitted between the variable, the equal sign, and the value. If spaces exist in a variable assignment, the shell tries to execute either the variable name of the value as can be seen:
$ COUNT =1 COUNT: not found $ COUNT= 1 1: execute permission denied $ COUNT=1 $

To retrieve the value stored in a variable, the variable must be preceded by a dollar sign ( $). Getting the value is called dereferencing.
$ COUNT=1 $ echo $COUNT 1

Just as the name of a variable cannot contain white space, neither may the value.
$ NAME=John Doe

19 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Doe: not found

But with the help of the shell's quoting mechanisms, it is possible to create strings that do contain white space and then assign them to a variable's value.
$ NAME="John Doe" $ echo $NAME John Doe $ NAME='Jane Doe' $ echo $NAME Jane Doe

Moreover, the same quoting techinques can be used to place quotes in a variable's value.
$ ERROR_MSG="You can't do that!" $ echo $ERROR_MSG You can't do that! $ A_QUOTE='"To be, or not to be"' $ echo $A_QUOTE "To be, or not to be"

Using quotes, it is even possible to store the output of a command into a variable.
$ XTERMINALS=`ypcat hosts | grep ncd[0-9][0-9] | \ > cut -f2 | cut -f2 -d" " | uniq` $ echo "The NCD xterminals are $XTERMINALS" The NCD xterminals are ncd05 ncd02 ncd03 ncd01 ncd07 ncd04 ncd06

Filename expansion can be used to assign values to variables. The usual rules for metacharacters apply.
$ XPROGS=/usr/bin/x* $ echo $XPROGS /usr/bin/xargs /usr/bin/xgettext /usr/bin/xstr $ IPROGS=/usr/bin/i[0-9]* $ echo $IPROGS /usr/bin/i286 /usr/bin/i386 /usr/bin/i486 /usr/bin/i860 /usr/bin/i86pc

Braces can be used during variable dereferencing. It is highly recommended that programmers consistently delimit variables with braces. The technique has a few advantages. First, it makes variables standout within a script. Readers can easily see the variable being dereferenced within a block of code. Second, it has the advantage of permitting the generation of expanded strings from variable values. If a script declared a variable named FILE, it could then make a backup copy of the actual value stored in FILE:
$ FILE=oracle.log $ cp ${FILE} ${FILE}.bak $ ls oracle.log oracle.log.bak

This technique would work without the braces, but notice the difference between using the braces and not using them.
$ cp $FILE $FILE.bak

In this case, is it obvious to the reader that the second dereference is the value of FILE with .bak appended to it, or might the reader think that the variable being dereferenced is FILE.bak? By using braces to delimit the variable, the problem is solved. Certainly it is clear that the variable is FILE not FILE.bak. Moreover, there are times when appending text to the variable name may produce unexpected results.
$ cp $FILE $FILE2 cp: Insufficient arguments (1) Usage: cp [-i] [-p] f1 f2 cp [-i] [-p] f1 ... fn d1 cp [-i] [-p] [-r] d1 d2

20 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Here, the user attempts to create a copy of oracle.log into oracle.log2, but since digits can be part of a variable name, the shell tries to dereference the undefined variable FILE2. Third, braces allow parameter substitution. Parameter substitution is a method of providing a default value for a variable in the event that it is currently null. The construct uses a combination of braces delimiting a variable and its default. The variable and default value are separated by a keyword. The keyword serves as a condition for when to assign the value to the variable. The list of keywords is shown in the following table. Table 3.2-1. Summary of Parameter Substitution Meaning Substitue the value of parameter. Substitue the value of parameter if it is not null; otherwise, use value. Substitue the value of parameter if it is not null; otherwise, use value and also assign value to parameter. Substitue the value of parameter if it is not null; otherwise, write value to standard error and exit. If value is omitted, then write "parameter: parameter null or not set" instead. Substitue value if parameter is not null; otherwise, substitue nothing.

Construct
$parameter ${parameter} ${parameter:-value} ${parameter:=value}

${parameter:?value}

${parameter:+value}

Supposing a script tries to dereference a null variable, a good programmer can avoid catastrophic errors by using parameter substitution:
$ echo $NULL $ echo "Is it null? : ${NULL} :" Is it null? : : $ echo "Is it null? : ${NULL:-Nope} :" Is it null? : Nope : $ echo "Actually it still is null : ${NULL:?} :" NULL: parameter null or not set $ echo "We'll take care of that : ${NULL:=Nope} :" We'll take care of that : Nope : $ echo "Is it null? : ${NULL} :" Is it null? : Nope :

3.3 Properties of Variables


First and foremost, the shell has no concept of data types. Some programming languages such as C++ or Pascal are strongly typed. In these languages, variables are grouped into type classes such as integer, character, and real. Typed variables expect a specific format for their value, and generally, they only operate within their class. For example, a real valued variable does not add well with a character. For the Bourne shell, there is no such restriction because every variable is simply a string of characters. This is why the results of commands and filename expansions are as legal as typing in values by hand. For the shell, there is no concept of integers, reals, or characters. The shell does not understand that COUNT=1 means that COUNT has an integral value. It only knows that the variable holds a single character string with the ASCII value for 1. Variables are read-write by default. Their values may be dereferenced and reset at will. If the need arises, a variable can be declared to be read-only by using the readonly command. The command takes a previously declared variable and applies write protection to it so that the value may not be modified.

21 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ NOWRITE="Try to change me" $ readonly NOWRITE $ echo ${NOWRITE} Try to change me $ NOWRITE="Ugh, you changed me" NOWRITE: is read only

It is important to remember that once a variable is declared read-only, from that point on, it is immutable. Therefore, it is equally important to set the variable's value before applying write protection. The scope of variables within a script is important to consider as well. Variables defined by a shell script are not necessarily visible to subshells the program spawns. Given a variable x defined in a shell session and given a script local executed during the same session, the value of x is undefined for script local:
$ x=World $ echo ${x} World $ cat local #!/bin/sh echo "Hello ${x}" $ local Hello

On the other hand, it is possible to instruct a shell session to publish its variables to all of its subshells. Called variable exporting, the technique uses the export command. Export takes a list of variables as its arguments. Each variable listed gains global scope for the current session.
$ cat local2 #!/bin/sh echo "${y} ${z}" $ y=Hello $ z=World $ local2 $ export y z $ local2 Hello World

In the example above, variables y and z are invisible to local2 as shown by the first invocation, but after applying export to both variables, local2 gains access to them as if it had declared them itself. Now eventhough a shell can publish its variables to subshells, the converse does not hold. Subshells cannot promote local variables to global scope.
$ cat local3 #!/bin/sh a=foo export foo $ b=bar $ echo "${a} ${b}" bar $ local3 $ echo "${a} ${b}" bar

Although local3 attempts to export a to its parent shell, the active session, a does not gain visibility. Only the subshells of local3 would be able to use a. Furthermore, subshells retain a copy of exported variables. They may reassign the value of a global variable, but the reassignment is not permanent. Its effects last only within the subshell. The global variable regains its original value as soon as control is returned to the parent shell.
$ cat local4 #!/bin/sh echo "var1 = ${var1}" var1=100 echo "var1 = ${var1}" $ var1=50 $ export var1 $ local4

22 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

var1 = 50 var1 = 100 $ echo ${var1} 50

The listing of local4 shows that it prints var1's value and then assigns a value of 100 to it. Var1 is first set to 50 and then exported by the active shell. Local4 is then executed. The output shows that the variable is correctly exported and has its value changed to 100. At this point, the script terminates, and control returns to the interactive session. A quick echo of var1 shows that the global instance of the variable has not changed.

3.4 Environment Variables


The Bourne shell predefines a set of global variables known as environment variables. They define default values for the current account's home directory, the strings to use as the primary and secondary prompts, a list of directories to search through when attempting to find an executable, the name of the account, and the current shell, to name a few. The values for these variables are generally set at login when the shell reads the
.login

script. This script resides within the account's home directory. Environment variables are by no means set in stone. They may be changed from session to session, but generally, they remain constant and keep a user's account configured for the system. A short listing of some common environment variables is shown in Table 3.4-1. Table 3.4-1. Common Environment Variables Meaning The absolute path to the user's home directory. The internal field separator characters. Usually contains space, tab, and newline. A colon separated list of directories to search through when trying to execute a command. The strings to use as the primary, PS1 ($), and secondary prompts, PS2 (>). The current working directory. The absolute path to the executable that is being run as the current shell session. The name of the current account.

Variable
HOME IFS PATH PS1 PS2 PWD SHELL USER

Changing an environment variable is as easy as assigning it a new value. As an example, the current directory could be appended to PATH's value. Doing so would allows a user to execute programs in the current directory without specifying a fully qualified path to the executable.
$ echo ${PATH} /bin:/usr/bin:/usr/ucb/bin $ PATH=${PATH}:${PWD} $ echo ${PATH} /bin:/usr/bin:/usr/ucb/bin:/home/rsayle

3.5 Special Parameters


In addition to predefining environment variables, the shell accepts a set of built-in variables known as special parameters. There are two types. The first type provides a method for accessing a set of arguments. When a command is issued at the prompt or a function is called, it often takes a set of parameters that define what it operates on or how to perform its operations. The script or function retrieves this argument list by using the positional parameters.
$ cat show-pos-params

23 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

#!/bin/sh echo ${1} ${2} ${3} ${4} ${5} ${6} ${7} ${8} ${9} $ show-pos-params testing 1 2 3 ... testing 1 2 3 ... $ show-pos-params One, two, buckle your shoe, three four shut the door One, two, buckle your shoe, three four shut the

There are nine positional parameters available, and each is accessible by dereferencing the argument number. The script show-pos-params listed above demonstrates how to use the positional parameters. The example also shows that only nine arguments can be handled by a script, or a function, at any time. In the second invocation of show-pos-params, the tenth argument, door, does not get printed. It has not vanished and is still available to the script; it is just not readily available via the positional parameters. This could be remedied by shifting the positional parameters so that door moved from the tenth to the ninth position. Shifting is discussed in Section 6, Looping. An astute reader might now ask about ${0}. Zero is a special positional parameter. It always holds the name of the executing program. Usually, it also includes a path to the program.
$ cat show-pos-param-0 #!/bin/sh echo "This script's name is ${0}" $ show-pos-param-0 This script's name is ./show-pos-param-0

The second set of special parameters look a little strange. They seem more like punctuation than variables because each special parameter is a symbol. They are, however, extremely useful for manipulating arguments, processes, and even the currently executing shell. Table 3.5-1. The Special Shell Parameters Parameter
$# $* $@ $$ $! $? $-

Usage The number of arguments passed to the shell or the number of parameters set by executing the set command. Returns the values of all the positional parameters as a single value. Same as $* except when double qouted it has the effect of double quoting each parameter. Returns the process id of the current shell session or executing script. Returns the process id of the last program sent to the background. Returns the exit status of the last command not executed in the background. Lists the current options in effect (same as executing set with no arguments).

A quick example shows how these parameters behave. The succeeding chapters will demonstrate their use extensively.
$ cat show-special-params #!/bin/sh echo "There are $# arguments: $*" echo Or if we needed them quoted, they would be: "$@" echo "If I wanted to backup this file, I might use ${0}.$$" echo "The last echo had exit status = $?" $ show-special-params with some arguments There are 3 arguments: with some arguments Or if we needed them quoted, they would be: with some arguments If I wanted to backup this file, I might use ./show-special-params.2163 The last echo had exit status = 0

"arguments". This passed to echo

Notice that in the second echo command, with some arguments was not printed as "with" "some" is a result of how echo treats quoted values; however, with some arguments is actually as separately quoted values. This has the advantage of protecting the original parameter values. Without double quotes, the shell could interpret what was meant to be distinct values as one value.

24 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

3.6 Reading Data into Variables


Data can be stored into user defined variables by assignment, as has already been shown, interactively, or from a file. To have a user provide a value or to get the value from a file, the read command must be used.
Read

takes a list of variables as its arguments. When being used interactively, it suspends program execution and awaits user input. After the user enters some text, read scans the line of input and seequentially assigns the words found to each of the variables in the list.
$ cat readit #!/bin/sh TIC_TAC="tic tac" echo "${TIC_TAC} ?" read ANSWER echo "${TIC_TAC} ${ANSWER}" $ readit tic tac ? toe tic tac toe

Above, readit pauses after printing tic tac ? On the next line, the user enters toe. Read receives the value from standard input and stores it in the new variable ANSWER which becomes immediately usable by the program. For read, words are delimited according to the characters defined by the internal field separator variable, IFS. It is usually set to be any white space. The example shows an interactive application; read may also be used to extract values from a file. To do this, however, requires advanced knowledge of how to combine looping constructs with I/O. This is explained later in Section 8.1, More I/O. Continuing with read's behavior when used interactively, if there are not enough words listed in the line of input, the remaining variables are simply left null.
$ cat not-enough-args #!/bin/sh read a b c d e echo "e = ${e}" $ not-enough-args There aren't enough variables e =

On the other hand, the last variable gets all the extra words if the list of variables is shorter than the available words.
$ cat more-than-enough-args #!/bin/sh read a b c echo "c = ${c}" $ more-than-enough-args There are more than enough variables c = more than enough variables

The point here is that programmers should be aware that unexpected results can occur quite easily when using the read command. It is therefore important to provide rudimentary checks on a variable after reading its value. Section 5, Branching presents the tools to perform such checks.

4. Functions

25 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

1. 2. 3. 4.

Declaring Functions Invoking Functions Properties of Functions Including Functions in Multiple Scripts

4.1 Declaring Functions


The Bourne shell allows the grouping of commands into a reusable instruction set called a function. Functions have two parts: a label and a body. The label names the function and is used to invoke the function. The body is the list of commands that the function executes. Here is an example function called setEnvironment. Its body is the three statements that set the values of the variables ROOT, LIB, and BIN.
setEnvrionment () { ROOT=${PWD} LIB=${ROOT}/lib BIN=${ROOT}/bin }

There exists a few rules that must be followed in order to properly declare a function: 1. The function label must be unique; it cannot be the same as any other variable or other function. 2. An empty set of parentheses must always follow the function label. They instruct the shell that a function is being defined. 3. Braces, {}, must be used to delimit the function's body. 4. A function must be defined before it can be used in a script. The second, third, and fourth rules are mandatory. Frankly speaking, without following them, a script will not recognize a declaration as a function. The first one, however, is not as strict. A script can declare a variable and a function by the same name, but the results are not one what might expect:
$ FUNC="This is a variable" $ FUNC () { > echo "This is a function" > } $ echo ${FUNC} bad substitution $ FUNC This is a function

The functional declaration of FUNC appears to be the active one, but is this always the case? If a function and a variable have the same name, will the function always be the active handle?
$ FUNC () { > echo "This is a function" > } $ FUNC="This is a variable" $ echo ${FUNC} This is a variable $ FUNC FUNC: not found

Apparently, the answer is no. Closer inspection of the examples shows that in both cases the shell uses the last declaration. Hence, a corollary to rule one states that in the event that a variable and a function have the same name, the most recent declaration becomes active. For the sake of completeness, the same thing can be said about declaring two variables with the same name or two functions with the same label. The bottom line remains: a scripter should choose unique function and variable labels.

4.2 Invoking Functions


26 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

To use a function in a script or an interactive session, a user must define it and then call it as if it was another command invokable from the shell prompt.
$ cat example-func #!/bin/sh setEnvironment () { ROOT=${PWD} LIB=${ROOT}/lib BIN=${ROOT}/bin } echo "Trying to print environment..." echo ${ROOT} ${LIB} ${BIN} setEnvironment echo "Trying to print environment again..." echo ${ROOT} ${LIB} ${BIN} $ example-func Trying to print environment... Trying to print environment again... /home/rsayle /home/rsayle/lib /home/rsayle/bin

The fact that the parentheses following the function label is empty is a bit misleading. Normally, in languages such as C and C++, the parentheses delimit an argument list, and an empty set of parentheses indicates that there are no arguments. But for the shell, the parentheses are just the method of declaration. In fact, all Bourne shell functions accept arguments. These arguments are accessible using positional parameter syntax.
$ cat func-args #!/bin/sh setEnvironment () { ROOT=${1} LIB=${ROOT}/lib BIN=${ROOT}/bin } setEnvironment /tmp echo "Trying to print environment..." echo ${ROOT} ${LIB} ${BIN} $ func-args Trying to print environment... /tmp /tmp/lib /tmp/bin

Comparing this to the previous example, this time setEnvironment does not hard code ROOT's value. It uses the first argument passed to the function. The invocation shows the results. With /tmp acting as the parameter, setEnvironment assigns values as expected. On the surface, shell functions appear more like procedures or routines. After all, a function generally returns a value. Actually, every shell function does return a value; although, it may not be readily apparent what the value is. Functions return the exit status of the last command executed much like when a script exits with no exit command; it uses the status of the last command issued. This similarity goes further. A script controls its exit status by issuing an exit with a non-negative value. On the other hand, functions do not use exit because it is designed to terminate the shell. Instead, functions use return. The return command stops execution of a function returning program control to the point in the script where the function was called. Script execution continues from where the function was invoked. The format of return follows return n where n is any non-negative integer. Providing a return value is optional just as providing a status code to exit is optional. If no code is given, return defaults to the value returned by the last command executed.
$ > > > isADir () { if [ ! -d ${1} ]; then return 1 fi

27 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

> $ $ 0 $ $ 1

} isADir ${HOME} echo $? isADir notADir echo $?

The exercise above declares the function isADir. The function checks an argument passed to it and determines if the argument's value represents a directory as shown by the function's first statement. If it is not a directory, the function returns an error value of one; otherwise, it should return zero as given by the execution of the if statement. The function is run twice. First with the home directory as its argument and, second, with a fictitious directory name. The special parameter $? is printed in order to show the return values of each trial. The short function just examined is a good one to demonstrate how to combine functions with test conditions. Many programmers familiar with traditional languages understand that a function can be embedded within a test in order to provide branching. The technique has the advantage of allowing multiple commands within one line for the sake of compactness. In some cases, it can also improve program efficiency by storing the value temporarily instead of having to create a persistent variable to hold the results. The same technique can be employed by the shell, but it is important to realize that quotes must be used.
$ if [ isADir ${HOME} ]; then > echo yep > fi test: argument expected $ if [ "isADir ${HOME}" ]; then > echo yep > fi yep

So it is perfectly legal to use functions as arguments themselves, but the syntax can become a bit unwieldy. Of course, the value can always be assigned to a variable and the variable checked, but there is a better alternative. Certainly, the special parameter $? provides access to a function's return value. It is therefore probably best to simply execute the function as a stand alone action and then immediately check the result using $?.

4.3 Properties of Functions


Aside from what has already been discussed about the behavior of functions, there are a few more considerations to keep in mind. First, functions are local to the currently executing shell. A function is never visible to a subshell or parent shell of the script in which it is defined; in other words, there is no export command for functions like there is for variables.
$ cat parent #!/bin/sh parentFunc () { echo "This is the parent's function" } parentFunc echo "Calling child..." child echo "Attempting to call childFunc from parent..." childFunc $ cat child #!/bin/sh childFunc () { echo "This is the child's function"

28 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

} childFunc echo "Attempting to call parentFunc from child..." parentFunc $ parent This is the parent's function Calling child... This is the child function Attempting to call parentFunc from child... ./child: parentFunc: not found Attempting to call childFunc from parent ./parent: childFunc: not found

The two scripts above demonstrate the fact that functions are local to the current shell context. Following the program's flow, the main shell, parent, declares a local function and calls it. The function displays a short message indicating that parentFunc executed. The program continues by calling the child subscript. Child begins execution imilarly. It also declares a local function, childFunc, which prints a message showing that the subshell's function ran. The subshell continues by trying to access parentFunc. Child prints the error message parentFunc: not found proving that a subshell cannot use functions declared by their parents. The subshell ends, and execution continues within parent at the echo. At this point, the supershell attempts to call childFunc. Again, the function is undefined for the current shell so it is also clear that supershells cannot use functions defined by their subshells. One might expect similar behavior to be true for changes to variables within functions. After all, in other programming languages, variables declared in functions are local to those functions, and generally speaking, variable values passed to functions are copied into temporary storage for local manipulation. But this is not the case. Changes made to variables persist beyond execution of the function. Moreover, variables declared in functions gain immediate global scope.
$ cat changes #!/bin/sh changeit () { AVAR="Changed Value" NEWVAR="New Value" } AVAR="Original Value" echo "AVAR = ${AVAR}" echo "NEWVAR = ${NEWVAR}" changeit echo "AVAR = ${AVAR}" echo "NEWVAR = ${NEWVAR}" $ changes AVAR = Original Value NEWVAR = AVAR = Changed Value NEWVAR = New Value

When this script first prints AVAR and NEWVAR, the variables have their initial values; namely AVAR equals Original Value and NEWVAR is null. The program then runs the changeit function which resets the variables to different values. The function ends and returns control to the main program. The same print commands are reissued, and a quick inspection of the output shows that AVAR's value is Changed Value and NEWVALUE is suddenly defined. To carry the examination of functional behavior even further, a third example considers nested functions. It is quite legal to declare functions within functions. Once again, a typical programmer might guess that a nested function is visible within the scope of its enclosing function. This guess is wrong. In the following example, the script nesting first declares afunc. Within afunc, the program defines the function nested. The script then calls nested, afunc, and nested yet again. A classical program would hide nested within afunc; it would be expected that the output from nested would never be seen. But the results show that as soon as afunc has run, nested has become visible to the script.
29 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ cat nesting #!/bin/sh afunc () { nested () { echo "This is the nested function" } echo "This is afunc" } nested afunc nested $ nesting ./nesting: nested: not found This is afunc This is the nested function

As the previous three examples demonstrate, shell function behavior is atypical. Yet as has been demonstrated, it is not unpredictable. An easy guideline to remember is that the usual scoping rules do not necessarily apply to shell functions. Aside from this simple rule, it takes practice and careful analysis of scripts to prevent errors caused by functions. With all this function bashing, one might wonder why functions should be used at all? First of all, the intention is not to discourage the use of fuctions. It is just a statement of the facts. On the contrary, functions are a very useful tool. The primary reason for using them is quite classical. Just as in every other programming language, functions allow scripters to organize actions into logical blocks. It is easier to think of a program a series of steps to perform and then to expand those steps into functions that perform the necessary actions. This is much better than trying to list every single command in order. The same commands instead can be grouped into functions, and then the program calls the functions. It is infintely easier to program this way and to follow the script's flow. In addition, functions can improve a script's performance. Rather than employ functions, a novice might consider grouping logical blocks into subscripts which the main script uses. This technique will work just fine, but the program's execution time will take a hit. When a script calls a subscript, the shell must find the subscript on disk, open it, read it into memory, and then execute it. This process happens every time a subscript is called eventhough the subscript may have been used previously. Functions are read once into memory as soon as they are declared. They have the advantage of o ne time read for multiple execution.
$ cat calls-subscript #!/bin/sh doit once doit twice doit thrice $ cat doit #!/bin/sh echo "${1}" $ cat calls-function #!/bin/sh doitFunc () { echo "${1}" } doitFunc once doitFunc twice doitFunc thrice

The example lists two scripts, calls-subscript and calls-function, that effectively do the same thing. Each prints the words once, twice, thrice, but the method they use to do the printing is different. Calls-subscript uses the script doit to print whereas calls-function uses the doitFunc function. The UNIX time program can be applied to the scripts in order to see how fast each performs.
$ time calls-subscript once

30 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

twice thrice real 0.2 user 0.0 sys 0.1 $ time calls-function

4. Functions
1. 2. 3. 4.

Declaring Functions Invoking Functions Properties of Functions Including Functions in Multiple Scripts

4.1 Declaring Functions


The Bourne shell allows the grouping of commands into a reusable instruction set called a function. Functions have two parts: a label and a body. The label names the function and is used to invoke the function. The body is the list of commands that the function executes. Here is an example function called setEnvironment. Its body is the three statements that set the values of the variables ROOT, LIB, and BIN. setEnvrionment () { ROOT=${PWD} LIB=${ROOT}/lib BIN=${ROOT}/bin } There exists a few rules that must be followed in order to properly declare a function: 1. The function label must be unique; it cannot be the same as any other variable or other function. 2. An empty set of parentheses must always follow the function label. They instruct the shell that a function is being defined. 3. Braces, {}, must be used to delimit the function's body. 4. A function must be defined before it can be used in a script. The second, third, and fourth rules are mandatory. Frankly speaking, without following them, a script will not recognize a declaration as a function. The first one, however, is not as strict. A script can declare a variable and a function by the same name, but the results are not one what might expect: $ FUNC="This is
a variable" $ FUNC () { > echo "This is a function" > } $ echo ${FUNC} bad substitution $ FUNC This is a function The functional declaration of FUNC appears to be the active one, but is this

always the case? If a function and a variable have the same name, will the function always be the active handle? $ FUNC () { > echo "This is a function" > } $ FUNC="This is a variable" $ echo ${FUNC} This is a variable $ FUNC FUNC: not found Apparently, the answer is no. Closer inspection of the examples shows that in both cases the shell uses the last declaration. Hence, a corollary to rule one states that in the event that a variable and a function have the same name, the most recent declaration becomes active. For the sake of completeness, the same thing can be said about declaring two variables with the same name or two functions with the same label. The bottom line remains: a scripter should choose unique function and variable labels.

4.2 Invoking Functions


To use a function in a script or an interactive session, a user must define it and then call it as if it was another command invokable from the shell prompt. $ cat example-func #!/bin/sh setEnvironment
() { ROOT=${PWD} LIB=${ROOT}/lib BIN=${ROOT}/bin } echo "Trying to print environment..." echo ${ROOT} ${LIB} ${BIN} setEnvironment echo "Trying to print environment again..." echo ${ROOT} ${LIB} ${BIN} $ example-func Trying to print environment... Trying to print environment again... /home/rsayle /home/rsayle/lib /home/rsayle/bin

The fact that the parentheses following the function label is empty is a bit misleading. Normally, in

31 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

languages such as C and C++, the parentheses delimit an argument list, and an empty set of parentheses indicates that there are no arguments. But for the shell, the parentheses are just the method of declaration. In fact, all Bourne shell functions accept arguments. These arguments are accessible using positional parameter syntax. $ cat func-args #!/bin/sh setEnvironment () { ROOT=${1} LIB=${ROOT}/lib
BIN=${ROOT}/bin } setEnvironment /tmp echo "Trying to print environment..." echo ${ROOT} ${LIB} ${BIN} $ func-args Trying to print environment... /tmp /tmp/lib /tmp/bin Comparing this to the previous example, this time setEnvironment does not hard code ROOT's value. It uses the first argument passed to the function. The invocation shows the results. With /tmp acting as the parameter, setEnvironment assigns values as expected.

On the surface, shell functions appear more like procedures or routines. After all, a function generally returns a value. Actually, every shell function does return a value; although, it may not be readily apparent what the value is. Functions return the exit status of the last command executed much like when a script exits with no exit command; it uses the status of the last command issued. This similarity goes further. A script controls its exit status by issuing an exit with a non-negative value. On the other hand, functions do not use exit because it is designed to terminate the shell. Instead, functions use return. The return command stops execution of a function returning program control to the point in the script where the function was called. Script execution continues from where the function was invoked. The format of return follows return n where n is any non-negative integer. Providing a return value is optional just as providing a status code to exit is optional. If no code is given, return defaults to the value returned by the last command executed.
$ isADir () { > if [ ! -d ${1} ]; then > return 1 > fi > } $ isADir ${HOME} $ echo $? 0 $ isADir notADir $ echo $? 1 The exercise above declares the function isADir. The function checks an

argument passed to it and determines if the argument's value represents a directory as shown by the function's first statement. If it is not a directory, the function returns an error value of one; otherwise, it should return zero as given by the execution of the if statement. The function is run twice. First with the home directory as its argument and, second, with a fictitious directory name. The special parameter $? is printed in order to show the return values of each trial. The short function just examined is a good one to demonstrate how to combine functions with test conditions. Many programmers familiar with traditional languages understand that a function can be embedded within a test in order to provide branching. The technique has the advantage of allowing multiple commands within one line for the sake of compactness. In some cases, it can also improve program efficiency by storing the value temporarily instead of having to create a persistent variable to hold the results. The same technique can be employed by the shell, but it is important to realize that quotes must be used.
$ if [ isADir ${HOME} ]; then > echo yep > fi test: argument expected $ if [ "isADir ${HOME}" ]; then > echo yep > fi yep

So it is perfectly legal to use functions as arguments themselves, but the syntax can become a bit unwieldy. Of course, the value can always be assigned to a variable and the variable checked, but there is a better alternative. Certainly, the special parameter $? provides access to a function's return value. It is therefore probably best to simply execute the function as a stand alone action and then immediately check the result using $?.

4.3 Properties of Functions


Aside from what has already been discussed about the behavior of functions, there are a few more considerations to keep in mind. First, functions are local to the currently executing shell. A function is never visible to a subshell or parent shell of the script in which it is defined; in other words, there is no export command for functions like there is for variables. $ cat parent #!/bin/sh parentFunc () { echo "This is the parent's function" } parentFunc echo "Calling child..." child echo "Attempting to call childFunc from parent..." childFunc $ cat child

32 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

#!/bin/sh childFunc () { echo "This is the child's function" } childFunc echo "Attempting to call parentFunc from child..." parentFunc $ parent This is the parent's function Calling child... This is the child function Attempting to call parentFunc from child... ./child: parentFunc: not found Attempting to call childFunc from parent ./parent: childFunc: not found The two scripts above demonstrate the fact that functions are local to the current shell context. Following the program's flow, the main shell, parent, declares a local function and calls it. The function displays a short message indicating that parentFunc executed. The program continues by calling the child subscript. Child begins execution imilarly. It also declares a local function, childFunc, which prints a message showing that the subshell's function ran. The subshell continues by trying to access parentFunc. Child prints the error message parentFunc: not found proving that a subshell cannot use functions declared by their parents. The subshell ends, and execution continues within parent at the echo. At this point, the supershell attempts to call childFunc. Again, the function is undefined for the current shell so it is also clear that supershells cannot use functions defined by their subshells. One might expect similar behavior to be true for changes to variables within functions. After all, in other programming languages, variables declared in functions are local to those functions, and generally speaking, variable values passed to functions are copied into temporary storage for local manipulation. But this is not the case. Changes made to variables persist beyond execution of the function. Moreover, variables declared in functions gain immediate global scope.
$ cat changes #!/bin/sh changeit () { AVAR="Changed Value" NEWVAR="New Value" } AVAR="Original Value" echo "AVAR = ${AVAR}" echo "NEWVAR = ${NEWVAR}" changeit echo "AVAR = ${AVAR}" echo "NEWVAR = ${NEWVAR}" $ changes AVAR = Original Value NEWVAR = AVAR = Changed Value NEWVAR = New Value When this script first prints AVAR and NEWVAR, the variables have their initial values; namely AVAR equals Original Value and NEWVAR is null. The program then runs the changeit

function which resets the variables to different values. The function ends and returns control to the main program. The same print commands are reissued, and a quick inspection of the output shows that AVAR's value is Changed Value and NEWVALUE is suddenly defined. To carry the examination of functional behavior even further, a third example considers nested functions. It is quite legal to declare functions within functions. Once again, a typical programmer might guess that a nested function is visible within the scope of its enclosing function. This guess is wrong. In the following example, the script nesting first declares afunc. Within afunc, the program defines the function nested. The script then calls nested, afunc, and nested yet again. A classical program would hide nested within afunc; it would be expected that the output from nested would never be seen. But the results show that as soon as afunc has run, nested has become visible to the script.
$ cat nesting #!/bin/sh afunc () { nested () { echo "This is the nested function" } echo "This is afunc" } nested afunc nested $ nesting ./nesting: nested: not found This is afunc This is the nested function

As the previous three examples demonstrate, shell function behavior is atypical. Yet as has been demonstrated, it is not unpredictable. An easy guideline to remember is that the usual scoping rules do not necessarily apply to shell functions. Aside from this simple rule, it takes practice and careful analysis of scripts to prevent errors caused by functions. With all this function bashing, one might wonder why functions should be used at all? First of all, the intention is not to discourage the use of fuctions. It is just a statement of the facts. On the contrary, functions are a very useful tool. The primary reason for using them is quite classical. Just as in every other programming language, functions allow scripters to organize actions into logical blocks. It is easier to think of a program a series of steps to perform and then to expand those steps into functions that perform the necessary actions. This is much better than trying to list every single command in order. The same commands instead can be grouped into functions, and then the program calls the functions. It is infintely easier to program this way and to follow the script's flow. In addition, functions can improve a script's performance. Rather than employ functions, a novice might consider grouping logical blocks into subscripts which the main script uses. This technique will work just fine, but the program's execution time will take a hit. When a script calls a subscript, the shell must find the

33 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

subscript on disk, open it, read it into memory, and then execute it. This process happens every time a subscript is called eventhough the subscript may have been used previously. Functions are read once into memory as soon as they are declared. They have the advantage of o ne time read for multiple execution.
$ cat calls-subscript #!/bin/sh doit once doit twice doit thrice $ cat doit #!/bin/sh echo "${1}" $ cat calls-function #!/bin/sh doitFunc () { echo "${1}" } doitFunc once doitFunc twice doitFunc thrice The example lists two scripts, calls-subscript and calls-function, that effectively do the same thing. Each prints the words once, twice, thrice, but the method they use to do the printing is different. Calls-subscript uses the script doit to print whereas calls-function uses the doitFunc function. The UNIX time

program can be applied to the scripts in order to see how fast each performs. $ time calls-subscript once twice thrice real 0.2 user 0.0 sys 0.1 $ time calls-function once twice thrice real 0.0 user 0.0 sys 0.0 The script that uses a function is faster. It is not even measurable by the system. On the other hand, calls-subscript executes on the order of a couple tenths of a second.

4.4 Including Functions in Multiple Scripts


The Bourne shell supports a construct similar to the #include macro found in C/C++ or the import command of Pascal. Called the dot command, a period instructs the shell to read the file named and to execute it as if the commands contained had been entered at that point. In other words, the shell treats the included file as part of the currently executing program. In shell terminology, this construct is called sourcing. The named file is sourced so that its environment and commands become an inherent part of the running shell.
$ cat checkDir

checkDirFunc () {
if [ ! -d ${1} ]; then echo "${1} is not a directory"; exit 1 fi

}
$ cat checkHome #!/bin/sh . checkDir checkDirFunc echo "Of course it's a directory!" $ checkHome ${HOME} Of course it's a directory! $ checkHome notAdir notAdir is not a directory

The example above uses two scripts: checkDir and checkHome. Notice that checkDir does not contain the
#!/bin/sh

directive. It is not meant to be run as a stand-alone program. (This does not prevent the file from being executed if called by a running shell. The script will simply be run within the shell. The only way to truly prevent it from being executed is to set the correct file permissions such that it is not an executable file.) On the other hand, checkHome is written to execute within its own shell. Moreover, checkHome sources checkDir as can be seen by the line . checkDir. By sourcing the checkDir file, checkHome then contains the checkDirFunc function which tests the first positional parameter to see if it is a directory. If it is not a directory, the if statement prints a message stating the fact and exits the program. If it is a directory, program execution continues past the function to the final echo statement letting the user know that the argument is a directory. The operation of the entire script is shown by the two examples of running the script against the user's home directory and then against the bogus string notAdir.

34 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

There is one final comparison that can be made here. The previous subsection discusses the advantage of using functions versus calling subscripts. Naturally, one might ask how sourcing functions compares to these. Consider three methods of counting to 1000. The first method declares a function called printCount:
$ cat calls-function #!/bin/sh COUNT=1 printCount () { echo ${COUNT} } until [ ${COUNT} -gt 999 ]; do printCount COUNT=`expr ${COUNT} + 1` done echo "`basename ${0}`: Done counting to 1000"

In the script calls-function, the printCount function simply displays the current value of COUNT. An until loop iincrements and monitors the value until it reaches a thousand by determining whether COUNT is greater than 999. At that point, the program terminates with a short message that it finished counting. Timing the script's execution yields the following results. (Note that the actual output has been trimmed to the results of time in the interest of saving space.)
$ time calls-function calls-function: Done counting to 1000 4.86user 6.57system 0:11.68elapsed

Now consider the second method of counting to 1000. Here, the script sources-function imports the same function, printCount, from the file function; it sources the code used to print the value of COUNT.
$ cat sources-function #!/bin/sh COUNT=1 . function until [ ${COUNT} -gt 999 ]; do printCount COUNT=`expr ${COUNT} + 1` done echo "`basename ${0}`: Done counting to 1000" $ cat function printCount () { echo ${COUNT} }

Once again, an until loop watches and terminates once COUNT reaches 1000. Comparing the execution time to calls-function, it can be seen that there is a slight performance penalty for sourcing. This can be explained by the fact that the file function, when it is sourced, has to be opened and its contents committed to memory before the function can be used. On the other hand, in calls-function the shell reads printFunction during its initial pass over the contents of the script. It does not incur the penalty of having to do the extra file read that sources-function does.

35 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ time sources-function sources-function: Done counting to 1000 4.96user 6.61system 0:11.80elapsed

Finally, compare the first two methods to calls-subscript. This last script exports COUNT and then uses the program subscript to display COUNT's value:
$ cat calls-subscript #!/bin/sh COUNT=1 export COUNT until [ ${COUNT} -gt 999 ]; do subscript COUNT=`expr ${COUNT} + 1` done echo "`basename ${0}`: Done counting to 1000" $ cat subscript #!/bin/sh echo ${COUNT}

Running calls-subscript through the time program shows:


$ time calls-subscript calls-subscript: Done counting to 1000 10.95user 13.37system 0:24.83elapsed

In this last example, there is a significant increase in the program's performance. It takes roughly twice as long to complete as the first two. This can be attributed to the fact that each time calls-subscript uses subscript, the shell must open the file, spawn a subshell, execute the echo statement, and then return control back to the main program. From this exercise it is plain to see that including function code directly into scripts is the most optimal. It is definitely a bad idea to divide scripts into many subscripts because of the performace penalty. Try to use functions whenever it seems like a subscript fits. Now there is something to be said for sourcing commands from another file. Sourcing does not hinder program execution much and it does allow the organization of reusable code blocks into script libraries. Still, these same code blocks could simply be copied directly into a script's body in the name of speed. But that is a matter of choice. After all, the enhancement of speed could come at the cost of readability.

5. Branching
1. Testing Conditional Statements 2. If Statements 3. Case Statements

5.1 Testing Conditional Statements


Decisions, decisions, decisions. One of the corner stones of every programming language is the ability to

36 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

make decisions. A decision can only be made based upon a proposition. In other words, if a proposition is true or false, then take appropriate action. Learning to evaluate a proposition in the shell requires mastery of the test command. Taking action based upon the proposition's result is discussed in the subsections and chapters that follow. The built-in command test evaluates logical expressions. It takes boolean statments as its argument and returns a value of zero or one meaning true and false, respectively. Looking at the statements below, for example, test can be used to compare the values of two constants.
$ $ 0 $ $ 1 test 100 -eq 100 echo $? test 100 -eq 50 echo $?

In the first case, the user checks if 100 is equal to 100. Note that test does not actually return the value directly to the user. Instead the user pulls the result from the $? special parameter. As can be seen, the constants are indeed identical since 0 was returned. The second case shows test returning a false value. Here the constants 100 and 50 are not equal so test returns 1. There is a shortened form of the test command. Enclosing an expression in square brackets, [], also tests the statement. To use them, place the expression between the brackets. Be certain to include white space between the brackets and the statement.
$ [ ${HOME} = /home/rsayle ] $ echo $? 0

The white space is mandatory for the shell to properly interpret the expression. Notice how the shell will misread the test condition as an executable file if the space is missing between the opening bracket and the first operand:
$ [${HOME} = /home/rsayle ] sh: [/home/rsayle: No such file or directory

Also notice that it does not find the test's termination point without the space between the final operand and the closing bracket:
$ [ ${HOME} = /home/rsayle] [: missing `]'

Always prefer brackets over the test command; it makes scripts easier to read. There are a number of operators that allow the formation of logical expressions for testing. Being that the shell itself operates primarily on strings, it follows, naturally, that there exist string operators. The shell can test a string to see if it is empty or non-empty or it can compare two strings for equivalence.

37 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Table 4.1-1. String Operators Construct


string -n string -z string string1 = string2 string1 != string2 string string string

Meaning is not null is not null is null is identical to string2 is not identical to string2

string1 string1

38 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

So here are some farily simple examples that demonstrate these operators.
$ [ "hello" = "world" ] $ echo $? 1 $ ASTRING="foo" $ [ ${ASTRING} ] $ echo $? 0 $ NULLSTRING="" $ [ ${NULLSTRING} ] $ echo $? 1 $ [ -n "${NULLSTRING}" ] $ echo $? 1 $ [ -z "${NULLSTRING}" ] $ echo $? 0 $ [ -z ${NULLSTRING} ] test: argument expected

The first test checks the equivalence of the strings hello and world using the = operator. The result shows that the strings are not the same. Then, the operator assigns a variable, ASTRING, the value foo. The user encloses the value in test brackets and retrieves the result. This construct tests the string for emptiness. If it has no value or is an empty string, denoted by "", test returns false. As long as it has some value, the string is considered non-empty, and test returns true. So testing ASTRING yields 0. On the other hand, the operator then declares an empty string, NULLSTRING, and checks it. This time test returns false. Now compare this to using -n which returns true if a string is not empty. The -n operator gives the same result, false, as shown by the fourth test. And just for fun, the user follows this by demonstrating the -z operator. Here, -z reports a true result because NULLSTRING in fact an empty string. Finally, notice the potential problem of testing strings. In the last command, the user passes the variable's value unquoted to the operator. Because the variable really has no value, the shell passes no argument to test. This results in an error. It is extremely important to get in the habit of quoting string variables. Novice scripters often assign values to variables by storing the result of a command. The variable's value is then dereferenced and parsed. If the command yields no value, then the variable is null. Any tests or commands performed on the null value result in an error and program termination. By quoting the variable during its dereference the shell treats it as an empty string instead of nothing at all, and in most cases, the program does not crash. Testing strings alone would make for a fairly inflexible system. The shell does contain facilities for testing other conditions. In fact, it can perform some routine integer comparisons; namely, the shell supports:
-eq -ne -gt -ge -lt -le

equal to, not equal to, greater than, greater than or equal to, less than, and less than or equal to.

These operators function the same as in any other programming language. They compare the left operand to the right operand and return a true or false value. As a quick example, here is a comparison between the variable TEN, whose value is 10, with various other values.
$ TEN=10

39 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ $ 0 $ $ 1 $ $ $ 0

[ ${TEN} -eq 10 ] echo $? [ ${TEN} -gt 10 ] echo $? ONEHUNDRED=100 [ ${TEN} -le ${ONEHUNDRED} ] echo $?

In addition to string and integral comparisons, the shell supports a vast array of file operators. One can test for the existence of a file, if the file is readable, writeable, or even executable, and for the file type. The full list of file checking capabilities is given in the table below. It should be noted that having built-in file testing is quite a natural extension of the shell. After all, the basic unit within UNIX is the file. Data, directories, hardware interfaces, I/O devices, and even network communications are represented by files. Because of this, the ability to manipulate files is key to proper system operation. This book has already discussed some file handling techniques including I/O redirection and the representation of scripts in the system. It now adds the built-in file testing facilities.

Operator
-b -c -d -f -g -k -p -r -s -t -u -w -x

Table 4.1-2. File Operators Returns true if... file is a block special file file is a character special file file is a directory file is an ordinary file file has its set group ID (SGID) bit set file has its sticky bit set file is a named pipe file is readable by the current process file has a non-zero length file descriptor is open and associated with a terminal file has its set user id (SUID) bit set file is writable by the current process file is executable by the current process

All of the file operators shown are unary. They only take a single file as an argument. To test multiple conditions on a file, a scripter must string together a list of expressions using the logical conjuctions, which are discussed below. Also, to negate an operator, simply precede the expression with an exclamation point.
$ ls -l total 42 drwxr-xr-x -rwxr-xr-x -rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r--

2 1 1 1 1 1 1 1

rsayle rsayle rsayle rsayle rsayle rsayle rsayle rsayle

users users users users users users users users

5120 313 1244 66 103 1246 1243 1244

Apr Apr Apr Apr Apr Apr Apr Apr

11 11 11 11 11 11 11 11

17:13 17:13 17:13 17:13 17:13 17:13 17:13 17:13

complete/ cpusers* goad-s.txt helco3.txt helco3tx.txt lawrenc-c.txt lien-v.txt magro-j.txt

40 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

-rw-r--r-1 rsayle -rwxr-xr-x 1 rsayle -rw-r--r-1 rsayle -rw-r--r-1 rsayle -rw-r--r-1 rsayle -rw-r--r-1 rsayle -rwxr-xr-x 1 rsayle -rw-r--r-1 rsayle -rw-r--r-1 rsayle -rw-r--r-1 rsayle -rw-r--r-1 rsayle $ [ -d complete ] $ echo $? 0 $ [ -d cpusers ] $ echo $? 1 $ [ -x mkconfig ] $ echo $? 0 $ [ -f users ] $ echo $? 0 $ [ ! -c users ] $ echo $? 0

users users users users users users users users users users users

1244 7048 1244 1243 1244 1241 1444 1244 1244 334 1243

Apr Apr Apr Apr Apr Apr Apr Apr Apr Apr Apr

11 11 11 11 11 11 11 11 11 11 11

17:13 17:13 17:13 17:13 17:13 17:13 17:13 17:13 17:13 17:13 17:13

mergard-j.txt mkconfig* mynes-c.txt nair-h.txt pricket-j.txt riely-c.txt setamdroute* short-j.txt spilo-d.txt users zein-a.txt

The previous example demonstrates the usage of the file operators. It begins with a detailed listing of the current directory. Comparing this to the tests shows how a few of the operators work. The first test checks
complete

to see if it is a directory. Looking at the file listing and then checking the returned result shows that it is indeed a directory; however, the script cpusers is not a directory as the next test proves. Next, the example shows that the mkconfig script is executable by using -x. Then the users file is first shown to be a plain text file with -f. The example ends with an demonstration of how to take the complement of a file operator. Notice that by entering ! -c (read "not -c") the test verifies that users is not a character special device. This is a true statement as both the result and the preceding test on users show. Now in order to build more meaningful test conditions, it may be necessary to join two or more expressions. The shell achieves this by providing logical AND, -a, and OR, -o. Once again, the usual rules of logic taught in math classes around the globe apply. In the case of -a, both conditions surrounding the conjunction must be true for test to return a true value. For -o, either expression may be true for test to return true. To use -a or -o, simply place the conjunction between the boolean statements. Returning to the previous example of comparing files:
$ $ 0 $ $ 0 $ $ 1 [ -f setamdroute -a -x setamdroute ] echo $? [ -c users -o -d complete ] echo $? [ -c users -a -d complete ] echo $?

Note that any combination of file, integral, or string comparisons can be joined by -a and -o, but for

41 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

demonstration, this example sticks to testing files. So the first test checks that setamdroute is an ordinary text file and that it is executable. Since both statements are true, the result is true. The example continues by verifying that either users is a character special file, which it is not, or that complete is a directory. Since complete is a directory, test returns true. Finally, the test now changes to an AND expression to show that both conditions must be true; otherwise, test returns false. As a final discussion of test, a good scripter must know how to force a desired condition. Namely, set the value of the expression so that the test always returns true or false as desired. Most UNIX machines have two commands that do exactly this. They are aptly named true and false. Whenever a condition should be forced, these commands can be substituted in place of an actual test. For example, a system administrator might want to monitor a printing queue. Being a master scripter, said administrator would initiate a while loop that calls the lpq command every second so that he can watch for jobs. In order to keep the while loop running, he passes the true command to the loop.
$ while true > do > lpq -Plp; sleep 1 > done no entries no entries no entries ^C

Another method of forcing a condition is to use the null command. Represented by a colon ( :), the null command is a do nothing statement that the always returns zero. It can be used in place of a test condition like true and false. It may also be used as an empty statement where the shell expects something, but the user does not necessarily want any processing to occur. This cou ld be useful, for example, during development of a branch when a user might just want to test the condition before determining the command block that executes.
$ while : > do > lpq -Plp; sleep 1 > done no entries no entries no entries ^C

So there is the same print queue monitoring script entered by the now lazy system administrator who figures that saving the extra few key strokes means a shorter trip to carpal-tunnel hell. Actually, this is a very common technique. It may not be as readable as the word true, but it gets the job done. And here is an example of where a scripter might not yet know what command set should be executed after a certain test is satisfied.
if [ "${USER}" = "root" ]; then : else echo "`basename ${0}`: you must be root to run this program." exit 1 fi

The if statement makes sure that the user is the root user. The scripter may not know exactly what steps to take once this is verified, but for quick testing, it is left blank with a :. With this in place, the user can actually run the script to ensure that if the user is not root, then the program exits with an error.

42 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

5.2 If Statements
Now that it has been shown how to make decisions, it is time to learn how to act based upon its result. This is called branching. The first branching method uses the if-then-elif-else-fi construct. Read elif as "else if;" it makes more sense. An if statement reads similarly to classical logic. If something is true, then do the following, or if another statement is true, then do this instead; otherwise, just do this. Sentences that can be written in this fashion can be easily translated into an if block. The if-then-elif-else-fi block has the following syntax:
if [ condition ]; then command1 : commandn elif [ condition ]; then command1 : commandn : : elif [ condition ]; then command1 : commandn else command1 : commandn fi

This construct may appear to be long-winded, but it is actually pretty simple to analyze. The ifs and elifs are pretty much the same. An if or elif is followed by a test. Whenever the test evaluates to true, the shell executes the commands that follow and then continues with the rest of the script after the block's end point, the fi. The else statement is a default action. Given that neither of the if or elifs pass, the shell performs the commands within the else block, and the script again continues execution after the fi. Usage of elifs and elses in an if statement are optional. As a bare minimum, an if block must utilize an if-then-fi construct. Here is a popular example:
if [ "${USER}" != "root" ]; then echo "Sorry, ${USER}, you must be the superuser to run this program." exit 1 fi echo "Executing program."

This simple little subroutine checks if the current operator is the root user. If not, then it prints a message indicating that the script may only be executed by those with root privileges and promptly terminates. In the event that the operator is logged in as root, the script moves to the fi and then prints a friendly message indicating that it is continuing execution. A more advanced example might look like:
if [ -x ${FILE} ]; then ${FILE} elif [ -w ${FILE} ]; then vi ${FILE}

43 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

else echo "${FILE} is neither executable nor writable; don't know what to do!" exit 1 fi

Given a file, if the file is executable, the first part of the block causes the shell to run the program. But if it is not executable, the block continues by testing it for readability. If it turns out FILE is writable, it loads it into the vi editor. Should both tests fail, the block writes an error message and terminates the script. blocks can be nested. In other words, it is perfectly legal to build if statements within ifs, elifs, or even else blocks. Nesting, however, should only be used as an absolute nece ssity. Nested blocks are difficult to read which, consequently, makes them harder to debug. Try to rewrite nested if blocks as a series of ifs and elifs that perhaps have more precise test conditions.
If

5.3 Case Statements


The second form of branching available is the case statement. A case differs from an if block in that it branches based upon the value of one variable. An if does not have such restrictions because it uses the test function, and a test can be any string of logical expressions as has been shown. The best way to illustrate this difference is to examine the syntax:
case (variable) in pattern1) command1 : commandn ;; : patternn) command1 : commandn ;; esac

The case block takes as its argument a variable. To denote the variable, it must be surrounded by parentheses. The case compares the variable's value against each pattern. The pattern may be any legal regular expression. If the variable's value matches the pattern, then the shell executes the command block immediately following the pattern. The command block terminates with a pair of double semi-colons. As soon as it reaches them, the shell continues past the end of the case block as denoted by the esac. A good use for a case statement is the evaluation of command line arguments:
option="${1}" case ${option} in -f) FILE="${2}" ;; -d) DIR="${2}" ;; *) echo "`basename ${0}`: usage: [ -f filename ] | [ -d directory ]" exit 1 ;; esac

44 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

In this example, the script expects two arguments. The first is one of two option indicators, -f or -d, that tell the program whether the second argument is a file or a directory. The case statement switches upon the first argument's value stored in option. If option equals -f, then it assigns the option's argument, the second positional parameter, to FILE. Then the scripts continues after the esac. On the other hand, if option is -d, the shell assigns the second argument's value to DIR and continues execution. Finally, this case statement has a default action. In the event the user does not use either the -f or -d options, it detects the error with the wild card, *, which matches any pattern. The script prints a message instructing the user how to use it properly, and then exits. Remember that a case switches against patterns. Programmers who can build regular expressions with ease should have no problem designing effective case blocks.

6. Looping
1. 2. 3. 4.

For Loops While Loops Until Loops Loop Control

6.1 For Loops


With functions, variables, branching, and soon looping under the proverbial belt, all the tools to create useful shell scripts are at hand. But first, one must understand how to build and control shell loops. A loop repeats a command block based upon a set of criteria that determines when to continue or terminate the loop. Loops are a hallmark of all programming languages because they allow computers to do with great precision and speed what humans are not so terribly great at: repeating actions. In the Bourne shell, the first type to consider is the for loop. A for loop processes multiple commands on a list of items. This is a bit non-traditional in that languages such as C and FORTRAN employ for loops as a sort of counting mechanism; the loop continues until the variable reaches a target value. But here, the for loop uses the intermediate variable to execute actions on a set of items such as files or the results of a command. In this way, the shell's for loop is a more flexible and useful than in most other languages. The syntax of a for loop is:
for variable in list do command1 : commandn done

The for loop's variable may be any legal shell variable. It need not be declared prior to the loop, and in fact, good shell programming practice mandates that the scripter should choose a unique variable that is not and will not be used elsewhere. This is punctuated by the fact that the variable does become global in the script. As the for loop executes the command block, the value of variable changes into the next item found in list. Items in list must be separated by white space (space, tab, or newline characters). The list may be generated by many different methods including: file expansion, variable substitution, positional parameters, command substitution, or by simply providing a list of strings immediately following the in statement.

45 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ for file in ./* > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text ./prologues: directory ./source: directory

Here, the for loop takes advantage of filename expansion. The ./* directs the loop to process all of the files located in the current directory. The intermediate variable used is named file. Its values are passed to the file command. This command gives a best guess description of what type of file the argument is. To differentiate the file command from the intermediate variable, the variable of course is surrounded by curly braces. As for the results, bin, ftp, prologues, and source are all directories; majordomo.txt is considered to be prose; mig29.ps is a PostScript document; and ph.dat is a plain ASCII file. The same results can be achieved using command substitution. Surely, by using ls in the working directory, the loop processes the same arguments:
$ for file in `ls` > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text ./prologues: directory ./source: directory

In fact, the commands passed to a for loop as a list can be as simple or complex as a scripter wishes; however, for readability and for ease of debugging, a programmer should consider assigning the command's output to a variable and then passing the variable's value as the loop's argument list.
$ $ > > > > > NOPASSWD=`ypcat passwd | grep "^[a-zA-Z0-9]*::" | cut -f1 -d:` for user in ${NOPASSWD} do mail ${user} << END Please set your password. You currently don't have one! END done

Here's a nice hackers' example, or perhaps a tool for a security conscious system administrator. The script begins by assigning to NOPASSWD the results of a rather lengthy command filter. The command passes the NIS password database to grep.
Grep

searches for any entry without a password. The passwd database c onsists of a set of fields delimited by colons. The second field is the account's encrypted password; hence, an account without a password would be denoted by an entry with a double colon immediately following the account name. The list grep generates is then passed to cut to parse out the offending account names from the database entry. After this building the list, the subsequent for loop sends an email to each user warning them that they must correct the security problem

46 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

they created. For the last consideration of for loops, the in list portion is optional. If omitted, the loop uses the positional parameters as its arguments by default. This is equivalent to writing, "for var in "$@" do ..." This technique should be used with care because it is not always immediately obvious to another reading the script what the arguments of the loop are.
$ cat printargs #!/bin/sh for arg do echo ${arg} done $ printargs foo bar * foo bar bin printargs

This simple example quickly illustrates the concept. The user executes the printargs script which echos the arguments back to the console. The arguments are the strings foo and bar as well as the contents of the cirrent directory as given by the shell's file expansion of *.

6.2 While Loops


A while loop is different from a for loop in that is uses a test condition to determine whether or not to execute its command block. As long as the condition returns true when tested, the loop executes the commands. If it returns false, the script skips the loop and proceeds beyond its termination point. Consequently, the command set could conceiveably never run. This is further illustrated by the loop's syntax:
while condition do command1 : commandn done

Because the loop starts with the test, if the condition fails, then the program continues at whatever action immediately follows the loop's done statement. If the condition passes, then the script executes the enclosed command block. After performing the last command in the block, the loop tests the condition again and determines whether to proceed beyond the loop or fall through it once more.
$ $ > > > > 1 2 3 4 5 i=1 while [ ${i} -le 5 ] do echo ${i} i=`expr ${i} + 1` done

47 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

The preceeding example demonstrates a while loop. Given the variable i whose initial value is one, the loop starts by checking if i is less than or equal to five. Since it is, the loop prints i's value, increments it by one as shown with the expr command, and then tests its value once more. This continues until i reaches six. At that point, the loop terminates, and the shell returns control to the user. The built-in command shift works well with while loops. Shift causes the positional parameters' values to left-shift by its argument. Shift can take any non-negative value as its argument and reset the positional parameters accordingly. By default,
shift

moves the arguments by one. When the parameters are shifted by one, the first is dropped from the list, the second parameter's value becomes that of the first, and third's becomes the second, and so on up to the ninth parameter. This technique can be used effectively with a while loop for processing script options or function arguments.
$ cat printargs #!/bin/sh while [ $# -gt 0 ] do echo "$@" shift done $ printargs hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

The printargs script combines a while loop with the shift command. The script takes an argument list and prints the entire list. After doing so, it then shifts the arguments by one and repeats the process until it shifts the arguments out of existence. The example demonstrates this with a four argument list of words. The third argument is two words passed as one variable by enclosing them in double-quotes.

6.3 Until Loops


Until loops are the other while loops. They work similarly in nature to while loops except for one major difference. An until loop executes its command block only if the test condition is false. In this sense, an until loop can be viewed as the logical negation of a while loop. until condition do command1 : commandn done

Once again, the command block may never be executed as shown in its syntax. If the condition evaluates to true, then the script proceeds beyond the loop. To demonstrate, the example below shows how to rewrite the printargs script above changing the while to an until loop:
$ cat printargs2 #!/bin/sh until [ $# -le 0 ] do echo "$@"

48 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

shift done $ printargs2 hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

6.4 Loop Control


Sometimes it is necessary to end a loop before the test condition is satisfied or a list is completely processed. In such cases, the scripter uses the break command. When encountered, the break command instructs the shell to continue processing beyond the enclosing loop. In other words, script execution picks up just after the done statement.
$ ls -l total 4 -rw-r--r-1 rsayle -rw-r--r-1 rsayle drwxr-xr-x 2 rsayle -rw-r--r-1 rsayle $ cat catfiles #!/bin/sh for file in ./* do if [ ! -d ${file} ]; echo "${file}:" cat ${file} echo else break fi done echo echo "End of catfiles"

users users users users

128 48 1024 89

Aug Aug Aug Aug

16 16 16 16

23:00 23:01 23:02 23:02

catfiles exrc include/ profile

then

For this example, the current working directory contains four objects: the script catfiles, an exrc file containing the user's vi defaults, an include directory holding a users header files, and a profile containing the user's shell default environment. Looking at catfiles, the script runs a for loop against all files in the working directory. If the file is not a directory, the script prints a line with the file's name followed by a colon. Then the program cats the file printing its contents to the screen. If the file is a directory, however, the script breaks the for loop and prints a termination message before exiting. Running catfiles against the directory produces:
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else break fi

49 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

done ./exrc: set number autoindent tabstop=2 showmode nowrap End of catfiles $

The files, as listed by the previous ls command, are processed in order. As the script encounters each file before the include directory, it cats the file. Once it hits include, the script completes leaving the contents of profile a mystery. Had the programmer intended to view all files except directories, the programmer would have used the continue command instead. When the shell encounters a continue it skips the current processing step and proceeds with the loop's next iteration. The loop does not just quit as it does with break. It just executes the next trial until the test condition is satisfied or the argument list is exhausted. Taking a look at catfiles using a continue instead of a break shows the difference. This time the script steps past the include directory and dumps the contents of profile.
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else continue fi done ./exrc: set number autoindent tabstop=2 showmode nowrap ./profile: # setup terminal characteristics export TERM=vt100 set -o vi # turn off mess

6. Looping
1. 2. 3. 4.

For Loops While Loops Until Loops Loop Control

6.1 For Loops


With functions, variables, branching, and soon looping under the proverbial belt, all the tools to create useful shell scripts are at hand. But first, one must understand how to build and control shell loops. A loop repeats a command block based upon a set of criteria that determines when to continue or terminate the loop. Loops are a hallmark of all programming languages because they allow computers to do with great

50 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

precision and speed what humans are not so terribly great at: repeating actions. In the Bourne shell, the first type to consider is the for loop. A for loop processes multiple commands on a list of items. This is a bit non-traditional in that languages such as C and FORTRAN employ for loops as a sort of counting mechanism; the loop continues until the variable reaches a target value. But here, the for loop uses the intermediate variable to execute actions on a set of items such as files or the results of a command. In this way, the shell's for loop is a more flexible and useful than in most other languages. The syntax of a for loop is:
for variable in list do command1 : commandn done

The for loop's variable may be any legal shell variable. It need not be declared prior to the loop, and in fact, good shell programming practice mandates that the scripter should choose a unique variable that is not and will not be used elsewhere. This is punctuated by the fact that the variable does become global in the script. As the for loop executes the command block, the value of variable changes into the next item found in list. Items in list must be separated by white space (space, tab, or newline characters). The list may be generated by many different methods including: file expansion, variable substitution, positional parameters, command substitution, or by simply providing a list of strings immediately following the in statement.
$ for file in ./* > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text ./prologues: directory ./source: directory

Here, the for loop takes advantage of filename expansion. The ./* directs the loop to process all of the files located in the current directory. The intermediate variable used is named file. Its values are passed to the file command. This command gives a best guess description of what type of file the argument is. To differentiate the file command from the intermediate variable, the variable of course is surrounded by curly braces. As for the results, bin, ftp, prologues, and source are all directories; majordomo.txt is considered to be prose; mig29.ps is a PostScript document; and ph.dat is a plain ASCII file. The same results can be achieved using command substitution. Surely, by using ls in the working directory, the loop processes the same arguments:
$ for file in `ls` > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text

51 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

./prologues: ./source:

directory directory

In fact, the commands passed to a for loop as a list can be as simple or complex as a scripter wishes; however, for readability and for ease of debugging, a programmer should consider assigning the command's output to a variable and then passing the variable's value as the loop's argument list.
$ $ > > > > > NOPASSWD=`ypcat passwd | grep "^[a-zA-Z0-9]*::" | cut -f1 -d:` for user in ${NOPASSWD} do mail ${user} << END Please set your password. You currently don't have one! END done

Here's a nice hackers' example, or perhaps a tool for a security conscious system administrator. The script begins by assigning to NOPASSWD the results of a rather lengthy command filter. The command passes the NIS password database to grep.
Grep

searches for any entry without a password. The passwd database c onsists of a set of fields delimited by colons. The second field is the account's encrypted password; hence, an account without a password would be denoted by an entry with a double colon immediately following the account name. The list grep generates is then passed to cut to parse out the offending account names from the database entry. After this building the list, the subsequent for loop sends an email to each user warning them that they must correct the security problem they created. For the last consideration of for loops, the in list portion is optional. If omitted, the loop uses the positional parameters as its arguments by default. This is equivalent to writing, "for var in "$@" do ..." This technique should be used with care because it is not always immediately obvious to another reading the script what the arguments of the loop are.
$ cat printargs #!/bin/sh for arg do echo ${arg} done $ printargs foo bar * foo bar bin printargs

This simple example quickly illustrates the concept. The user executes the printargs script which echos the arguments back to the console. The arguments are the strings foo and bar as well as the contents of the cirrent directory as given by the shell's file expansion of *.

6.2 While Loops


A while loop is different from a for loop in that is uses a test condition to determine whether or not to execute its command block. As long as the condition returns true when tested, the loop executes the commands. If it returns false, the script skips the loop and proceeds beyond its termination point. Consequently, the command set could conceiveably never run. This is further illustrated by the loop's syntax:

52 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

while condition do command1 : commandn done

Because the loop starts with the test, if the condition fails, then the program continues at whatever action immediately follows the loop's done statement. If the condition passes, then the script executes the enclosed command block. After performing the last command in the block, the loop tests the condition again and determines whether to proceed beyond the loop or fall through it once more.
$ $ > > > > 1 2 3 4 5 i=1 while [ ${i} -le 5 ] do echo ${i} i=`expr ${i} + 1` done

The preceeding example demonstrates a while loop. Given the variable i whose initial value is one, the loop starts by checking if i is less than or equal to five. Since it is, the loop prints i's value, increments it by one as shown with the expr command, and then tests its value once more. This continues until i reaches six. At that point, the loop terminates, and the shell returns control to the user. The built-in command shift works well with while loops. Shift causes the positional parameters' values to left-shift by its argument. Shift can take any non-negative value as its argument and reset the positional parameters accordingly. By default,
shift

moves the arguments by one. When the parameters are shifted by one, the first is dropped from the list, the second parameter's value becomes that of the first, and third's becomes the second, and so on up to the ninth parameter. This technique can be used effectively with a while loop for processing script options or function arguments.
$ cat printargs #!/bin/sh while [ $# -gt 0 ] do echo "$@" shift done $ printargs hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

The printargs script combines a while loop with the shift command. The script takes an argument list and prints the entire list. After doing so, it then shifts the arguments by one and repeats the process until it shifts the arguments out of existence. The example demonstrates this with a four argument list of words. The third argument is two words passed as one variable by enclosing them in double-quotes.

53 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

6.3 Until Loops


Until loops are the other while loops. They work similarly in nature to while loops except for one major difference. An until loop executes its command block only if the test condition is false. In this sense, an until loop can be viewed as the logical negation of a while loop. until condition do command1 : commandn done

Once again, the command block may never be executed as shown in its syntax. If the condition evaluates to true, then the script proceeds beyond the loop. To demonstrate, the example below shows how to rewrite the printargs script above changing the while to an until loop:
$ cat printargs2 #!/bin/sh until [ $# -le 0 ] do echo "$@" shift done $ printargs2 hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

6.4 Loop Control


Sometimes it is necessary to end a loop before the test condition is satisfied or a list is completely processed. In such cases, the scripter uses the break command. When encountered, the break command instructs the shell to continue processing beyond the enclosing loop. In other words, script execution picks up just after the done statement.
$ ls -l total 4 -rw-r--r-1 rsayle -rw-r--r-1 rsayle drwxr-xr-x 2 rsayle -rw-r--r-1 rsayle $ cat catfiles #!/bin/sh for file in ./* do if [ ! -d ${file} ]; echo "${file}:" cat ${file} echo else break

users users users users

128 48 1024 89

Aug Aug Aug Aug

16 16 16 16

23:00 23:01 23:02 23:02

catfiles exrc include/ profile

then

54 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

fi done echo echo "End of catfiles"

For this example, the current working directory contains four objects: the script catfiles, an exrc file containing the user's vi defaults, an include directory holding a users header files, and a profile containing the user's shell default environment. Looking at catfiles, the script runs a for loop against all files in the working directory. If the file is not a directory, the script prints a line with the file's name followed by a colon. Then the program cats the file printing its contents to the screen. If the file is a directory, however, the script breaks the for loop and prints a termination message before exiting. Running catfiles against the directory produces:
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else break fi done ./exrc: set number autoindent tabstop=2 showmode nowrap End of catfiles $

The files, as listed by the previous ls command, are processed in order. As the script encounters each file before the include directory, it cats the file. Once it hits include, the script completes leaving the contents of profile a mystery. Had the programmer intended to view all files except directories, the programmer would have used the continue command instead. When the shell encounters a continue it skips the current processing step and proceeds with the loop's next iteration. The loop does not just quit as it does with break. It just executes the next trial until the test condition is satisfied or the argument list is exhausted. Taking a look at catfiles using a continue instead of a break shows the difference. This time the script steps past the include directory and dumps the contents of profile.
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else continue fi done ./exrc: set number autoindent tabstop=2 showmode nowrap

55 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

./profile: # setup terminal characteristics export TERM=vt100 set -o vi # turn off mess

6. Looping
1. 2. 3. 4.

For Loops While Loops Until Loops Loop Control

6.1 For Loops


With functions, variables, branching, and soon looping under the proverbial belt, all the tools to create useful shell scripts are at hand. But first, one must understand how to build and control shell loops. A loop repeats a command block based upon a set of criteria that determines when to continue or terminate the loop. Loops are a hallmark of all programming languages because they allow computers to do with great precision and speed what humans are not so terribly great at: repeating actions. In the Bourne shell, the first type to consider is the for loop. A for loop processes multiple commands on a list of items. This is a bit non-traditional in that languages such as C and FORTRAN employ for loops as a sort of counting mechanism; the loop continues until the variable reaches a target value. But here, the for loop uses the intermediate variable to execute actions on a set of items such as files or the results of a command. In this way, the shell's for loop is a more flexible and useful than in most other languages. The syntax of a for loop is:
for variable in list do command1 : commandn done

The for loop's variable may be any legal shell variable. It need not be declared prior to the loop, and in fact, good shell programming practice mandates that the scripter should choose a unique variable that is not and will not be used elsewhere. This is punctuated by the fact that the variable does become global in the script. As the for loop executes the command block, the value of variable changes into the next item found in list. Items in list must be separated by white space (space, tab, or newline characters). The list may be generated by many different methods including: file expansion, variable substitution, positional parameters, command substitution, or by simply providing a list of strings immediately following the in statement.
$ for file in ./* > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text

56 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

./prologues: ./source:

directory directory

Here, the for loop takes advantage of filename expansion. The ./* directs the loop to process all of the files located in the current directory. The intermediate variable used is named file. Its values are passed to the file command. This command gives a best guess description of what type of file the argument is. To differentiate the file command from the intermediate variable, the variable of course is surrounded by curly braces. As for the results, bin, ftp, prologues, and source are all directories; majordomo.txt is considered to be prose; mig29.ps is a PostScript document; and ph.dat is a plain ASCII file. The same results can be achieved using command substitution. Surely, by using ls in the working directory, the loop processes the same arguments:
$ for file in `ls` > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text ./prologues: directory ./source: directory

In fact, the commands passed to a for loop as a list can be as simple or complex as a scripter wishes; however, for readability and for ease of debugging, a programmer should consider assigning the command's output to a variable and then passing the variable's value as the loop's argument list.
$ $ > > > > > NOPASSWD=`ypcat passwd | grep "^[a-zA-Z0-9]*::" | cut -f1 -d:` for user in ${NOPASSWD} do mail ${user} << END Please set your password. You currently don't have one! END done

Here's a nice hackers' example, or perhaps a tool for a security conscious system administrator. The script begins by assigning to NOPASSWD the results of a rather lengthy command filter. The command passes the NIS password database to grep.
Grep

searches for any entry without a password. The passwd database c onsists of a set of fields delimited by colons. The second field is the account's encrypted password; hence, an account without a password would be denoted by an entry with a double colon immediately following the account name. The list grep generates is then passed to cut to parse out the offending account names from the database entry. After this building the list, the subsequent for loop sends an email to each user warning them that they must correct the security problem they created. For the last consideration of for loops, the in list portion is optional. If omitted, the loop uses the positional parameters as its arguments by default. This is equivalent to writing, "for var in "$@" do ..." This technique should be used with care because it is not always immediately obvious to another reading the script what the arguments of the loop are.
$ cat printargs #!/bin/sh

57 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

for arg do echo ${arg} done $ printargs foo bar * foo bar bin printargs

This simple example quickly illustrates the concept. The user executes the printargs script which echos the arguments back to the console. The arguments are the strings foo and bar as well as the contents of the cirrent directory as given by the shell's file expansion of *.

6.2 While Loops


A while loop is different from a for loop in that is uses a test condition to determine whether or not to execute its command block. As long as the condition returns true when tested, the loop executes the commands. If it returns false, the script skips the loop and proceeds beyond its termination point. Consequently, the command set could conceiveably never run. This is further illustrated by the loop's syntax:
while condition do command1 : commandn done

Because the loop starts with the test, if the condition fails, then the program continues at whatever action immediately follows the loop's done statement. If the condition passes, then the script executes the enclosed command block. After performing the last command in the block, the loop tests the condition again and determines whether to proceed beyond the loop or fall through it once more.
$ $ > > > > 1 2 3 4 5 i=1 while [ ${i} -le 5 ] do echo ${i} i=`expr ${i} + 1` done

The preceeding example demonstrates a while loop. Given the variable i whose initial value is one, the loop starts by checking if i is less than or equal to five. Since it is, the loop prints i's value, increments it by one as shown with the expr command, and then tests its value once more. This continues until i reaches six. At that point, the loop terminates, and the shell returns control to the user. The built-in command shift works well with while loops. Shift causes the positional parameters' values to left-shift by its argument. Shift can take any non-negative value as its argument and reset the positional parameters accordingly. By default,
shift

58 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

moves the arguments by one. When the parameters are shifted by one, the first is dropped from the list, the second parameter's value becomes that of the first, and third's becomes the second, and so on up to the ninth parameter. This technique can be used effectively with a while loop for processing script options or function arguments.
$ cat printargs #!/bin/sh while [ $# -gt 0 ] do echo "$@" shift done $ printargs hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

The printargs script combines a while loop with the shift command. The script takes an argument list and prints the entire list. After doing so, it then shifts the arguments by one and repeats the process until it shifts the arguments out of existence. The example demonstrates this with a four argument list of words. The third argument is two words passed as one variable by enclosing them in double-quotes.

6.3 Until Loops


Until loops are the other while loops. They work similarly in nature to while loops except for one major difference. An until loop executes its command block only if the test condition is false. In this sense, an until loop can be viewed as the logical negation of a while loop. until condition do command1 : commandn done

Once again, the command block may never be executed as shown in its syntax. If the condition evaluates to true, then the script proceeds beyond the loop. To demonstrate, the example below shows how to rewrite the printargs script above changing the while to an until loop:
$ cat printargs2 #!/bin/sh until [ $# -le 0 ] do echo "$@" shift done $ printargs2 hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

59 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

6.4 Loop Control


Sometimes it is necessary to end a loop before the test condition is satisfied or a list is completely processed. In such cases, the scripter uses the break command. When encountered, the break command instructs the shell to continue processing beyond the enclosing loop. In other words, script execution picks up just after the done statement.
$ ls -l total 4 -rw-r--r-1 rsayle -rw-r--r-1 rsayle drwxr-xr-x 2 rsayle -rw-r--r-1 rsayle $ cat catfiles #!/bin/sh for file in ./* do if [ ! -d ${file} ]; echo "${file}:" cat ${file} echo else break fi done echo echo "End of catfiles"

users users users users

128 48 1024 89

Aug Aug Aug Aug

16 16 16 16

23:00 23:01 23:02 23:02

catfiles exrc include/ profile

then

For this example, the current working directory contains four objects: the script catfiles, an exrc file containing the user's vi defaults, an include directory holding a users header files, and a profile containing the user's shell default environment. Looking at catfiles, the script runs a for loop against all files in the working directory. If the file is not a directory, the script prints a line with the file's name followed by a colon. Then the program cats the file printing its contents to the screen. If the file is a directory, however, the script breaks the for loop and prints a termination message before exiting. Running catfiles against the directory produces:
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else break fi done ./exrc: set number autoindent tabstop=2 showmode nowrap End of catfiles $

The files, as listed by the previous ls command, are processed in order. As the script encounters each file before the include directory, it cats

60 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

the file. Once it hits include, the script completes leaving the contents of profile a mystery. Had the programmer intended to view all files except directories, the programmer would have used the continue command instead. When the shell encounters a continue it skips the current processing step and proceeds with the loop's next iteration. The loop does not just quit as it does with break. It just executes the next trial until the test condition is satisfied or the argument list is exhausted. Taking a look at catfiles using a continue instead of a break shows the difference. This time the script steps past the include directory and dumps the contents of profile.
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else continue fi done ./exrc: set number autoindent tabstop=2 showmode nowrap ./profile: # setup terminal characteristics export TERM=vt100 set -o vi # turn off mess

6. Looping
1. 2. 3. 4.

For Loops While Loops Until Loops Loop Control

6.1 For Loops


With functions, variables, branching, and soon looping under the proverbial belt, all the tools to create useful shell scripts are at hand. But first, one must understand how to build and control shell loops. A loop repeats a command block based upon a set of criteria that determines when to continue or terminate the loop. Loops are a hallmark of all programming languages because they allow computers to do with great precision and speed what humans are not so terribly great at: repeating actions. In the Bourne shell, the first type to consider is the for loop. A for loop processes multiple commands on a list of items. This is a bit non-traditional in that languages such as C and FORTRAN employ for loops as a sort of counting mechanism; the loop continues until the variable reaches a target value. But here, the for loop uses the intermediate variable to execute actions on a set of items such as files or the results of a command. In this way, the shell's for loop is a more flexible and useful than in most other languages. The syntax of a for loop is:

61 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

for variable in list do command1 : commandn done

The for loop's variable may be any legal shell variable. It need not be declared prior to the loop, and in fact, good shell programming practice mandates that the scripter should choose a unique variable that is not and will not be used elsewhere. This is punctuated by the fact that the variable does become global in the script. As the for loop executes the command block, the value of variable changes into the next item found in list. Items in list must be separated by white space (space, tab, or newline characters). The list may be generated by many different methods including: file expansion, variable substitution, positional parameters, command substitution, or by simply providing a list of strings immediately following the in statement.
$ for file in ./* > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text ./prologues: directory ./source: directory

Here, the for loop takes advantage of filename expansion. The ./* directs the loop to process all of the files located in the current directory. The intermediate variable used is named file. Its values are passed to the file command. This command gives a best guess description of what type of file the argument is. To differentiate the file command from the intermediate variable, the variable of course is surrounded by curly braces. As for the results, bin, ftp, prologues, and source are all directories; majordomo.txt is considered to be prose; mig29.ps is a PostScript document; and ph.dat is a plain ASCII file. The same results can be achieved using command substitution. Surely, by using ls in the working directory, the loop processes the same arguments:
$ for file in `ls` > do > file ${file} > done ./bin: directory ./ftp: directory ./majordomo.txt: English text ./mig29.ps: PostScript document ./ph.dat: ascii text ./prologues: directory ./source: directory

In fact, the commands passed to a for loop as a list can be as simple or complex as a scripter wishes; however, for readability and for ease of debugging, a programmer should consider assigning the command's output to a variable and then passing the variable's value as the loop's argument list.
$ NOPASSWD=`ypcat passwd | grep "^[a-zA-Z0-9]*::" | cut -f1 -d:` $ for user in ${NOPASSWD}

62 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

> > > > >

do mail ${user} << END Please set your password. END done

You currently don't have one!

Here's a nice hackers' example, or perhaps a tool for a security conscious system administrator. The script begins by assigning to NOPASSWD the results of a rather lengthy command filter. The command passes the NIS password database to grep.
Grep

searches for any entry without a password. The passwd database c onsists of a set of fields delimited by colons. The second field is the account's encrypted password; hence, an account without a password would be denoted by an entry with a double colon immediately following the account name. The list grep generates is then passed to cut to parse out the offending account names from the database entry. After this building the list, the subsequent for loop sends an email to each user warning them that they must correct the security problem they created. For the last consideration of for loops, the in list portion is optional. If omitted, the loop uses the positional parameters as its arguments by default. This is equivalent to writing, "for var in "$@" do ..." This technique should be used with care because it is not always immediately obvious to another reading the script what the arguments of the loop are.
$ cat printargs #!/bin/sh for arg do echo ${arg} done $ printargs foo bar * foo bar bin printargs

This simple example quickly illustrates the concept. The user executes the printargs script which echos the arguments back to the console. The arguments are the strings foo and bar as well as the contents of the cirrent directory as given by the shell's file expansion of *.

6.2 While Loops


A while loop is different from a for loop in that is uses a test condition to determine whether or not to execute its command block. As long as the condition returns true when tested, the loop executes the commands. If it returns false, the script skips the loop and proceeds beyond its termination point. Consequently, the command set could conceiveably never run. This is further illustrated by the loop's syntax:
while condition do command1 : commandn done

Because the loop starts with the test, if the condition fails, then the program continues at whatever action immediately follows the loop's done

63 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

statement. If the condition passes, then the script executes the enclosed command block. After performing the last command in the block, the loop tests the condition again and determines whether to proceed beyond the loop or fall through it once more.
$ $ > > > > 1 2 3 4 5 i=1 while [ ${i} -le 5 ] do echo ${i} i=`expr ${i} + 1` done

The preceeding example demonstrates a while loop. Given the variable i whose initial value is one, the loop starts by checking if i is less than or equal to five. Since it is, the loop prints i's value, increments it by one as shown with the expr command, and then tests its value once more. This continues until i reaches six. At that point, the loop terminates, and the shell returns control to the user. The built-in command shift works well with while loops. Shift causes the positional parameters' values to left-shift by its argument. Shift can take any non-negative value as its argument and reset the positional parameters accordingly. By default,
shift

moves the arguments by one. When the parameters are shifted by one, the first is dropped from the list, the second parameter's value becomes that of the first, and third's becomes the second, and so on up to the ninth parameter. This technique can be used effectively with a while loop for processing script options or function arguments.
$ cat printargs #!/bin/sh while [ $# -gt 0 ] do echo "$@" shift done $ printargs hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

The printargs script combines a while loop with the shift command. The script takes an argument list and prints the entire list. After doing so, it then shifts the arguments by one and repeats the process until it shifts the arguments out of existence. The example demonstrates this with a four argument list of words. The third argument is two words passed as one variable by enclosing them in double-quotes.

6.3 Until Loops


loops are the other while loops. They work similarly in nature to while loops except for one major difference. An until loop executes its command block only if the test condition is false. In this sense, an until loop can be viewed as the logical negation of a while loop.
Until until condition do

64 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

command1 : commandn done

Once again, the command block may never be executed as shown in its syntax. If the condition evaluates to true, then the script proceeds beyond the loop. To demonstrate, the example below shows how to rewrite the printargs script above changing the while to an until loop:
$ cat printargs2 #!/bin/sh until [ $# -le 0 ] do echo "$@" shift done $ printargs2 hello world "foo bar" bye hello world foo bar bye world foo bar bye foo bar bye bye

6.4 Loop Control


Sometimes it is necessary to end a loop before the test condition is satisfied or a list is completely processed. In such cases, the scripter uses the break command. When encountered, the break command instructs the shell to continue processing beyond the enclosing loop. In other words, script execution picks up just after the done statement.
$ ls -l total 4 -rw-r--r-1 rsayle -rw-r--r-1 rsayle drwxr-xr-x 2 rsayle -rw-r--r-1 rsayle $ cat catfiles #!/bin/sh for file in ./* do if [ ! -d ${file} ]; echo "${file}:" cat ${file} echo else break fi done echo echo "End of catfiles"

users users users users

128 48 1024 89

Aug Aug Aug Aug

16 16 16 16

23:00 23:01 23:02 23:02

catfiles exrc include/ profile

then

For this example, the current working directory contains four objects: the script catfiles, an exrc file containing the user's vi defaults, an include directory holding a users header files, and a profile containing the user's shell default environment. Looking at catfiles, the script runs a for loop against all files in the working directory. If the file is not a directory, the script prints a line with the file's name

65 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

followed by a colon. Then the program cats the file printing its contents to the screen. If the file is a directory, however, the script breaks the for loop and prints a termination message before exiting. Running catfiles against the directory produces:
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else break fi done ./exrc: set number autoindent tabstop=2 showmode nowrap End of catfiles $

The files, as listed by the previous ls command, are processed in order. As the script encounters each file before the include directory, it cats the file. Once it hits include, the script completes leaving the contents of profile a mystery. Had the programmer intended to view all files except directories, the programmer would have used the continue command instead. When the shell encounters a continue it skips the current processing step and proceeds with the loop's next iteration. The loop does not just quit as it does with break. It just executes the next trial until the test condition is satisfied or the argument list is exhausted. Taking a look at catfiles using a continue instead of a break shows the difference. This time the script steps past the include directory and dumps the contents of profile.
$ catfiles ./catfiles: #!/bin/sh for file in ./* do if [ ! -d ${file} ]; then echo "${file}:" cat ${file} else continue fi done ./exrc: set number autoindent tabstop=2 showmode nowrap ./profile: # setup terminal characteristics export TERM=vt100 set -o vi # turn off messaging mesg n

66 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

7. Handling Command Line Options


1. UNIX Command Line Option Styles 2. Getopts Statements

7.1 UNIX Command Line Option Styles


Options are switches to commands. They instruct the program to perform specific processing. Options appear on the command line following the command itself. They are recognizable by a preceding dash and come in two forms, separated and stacked: 1. separated: rdist -b -h -q -c /etc ren:/etc 2. stacked: ls -alg It should be noted that not all programs require their options to be preceded by a dash character. In such cases, the options are stacked and immediately follow the command. The ps and tar programs are such exceptions: ps waux and tar cvf /home/user /tmp/backup. Some options take mandatory arguments. When the program reads the option, it searches for the associated argument in order to process it accordingly. Usually, these options are listed separately with its argument as the next command line entry. An example is the make command used to compile a C program: make all -f build.mk -targetdir ./proj/bin. Here, the command line instructs make to build all parts of the program. It then tells make to take its build rules from the file build.mk as shown by the -f option. It also tells make to place the results in the directory ./proj/bin as given by the -targetdir option. The last switch shows that options need not just be single letters. Somtimes, programs interpret whole words as options. Not all commands require the separation of options and arguments. For some programs it may be perfectly fine to stack the options and then list their arguments respectively. Tar is a perfect example of this: tar cvbf 120 /home/user /tmp/backup. In the example, tar's first two arguments cause the program to create a new tape archive and use verbose mode so that the user can see what it is doing. Then the options with arguments are listed. The b option specifies the blocking factor to use, and the f option specifies the file, or directory in this case, to tar. The options' arguments then follow in order. Tar uses a blocking factor of 120 bytes per block and stores the contents of the user's home directory. Finally, the last argument to tar is the archive that it creates, /tmp/backup. Careful examination of a program's man page shows its legal option and argument list.

7.2 Getopts Statements


Considering the programming facilities presented thus far, it is plain to see that options are easy to handle using the positional parameters, a loop, some shifts, and a case statement:
#!/bin/sh # # setether: set an Ethernet interface's IP configuration # while [ $# -gt 1 ]

67 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

do case ${1} a) ARP="arp" shift ;; b) BROADCAST=${2} shift 2 ;; i) IPADDRESS=${2} shift 2 ;; m) NETMASK=${2} shift 2 ;; n) NETWORK=${2} shift 2 ;; *) echo "setether: illegal option: ${1}" exit 1 ;; esac done INTERFACE=${1} ifconfig ${INTERFACE} ${IPADDRESS} netmask ${NETMASK} broadcast ${BROADCAST} ${ARP} route add -net ${NETWORK}

The setether script processes a number of options and arguments in order to properly configure an Ethernet interface for use on an IP network. It uses a while loop and a case statement to do this. The scripts switches on the options using the case block. The options are the one letter switches and are expected by the script to be in the first position on the command line. Each option's argument is expected to follow the option in the second position. As setether processes the options, it shifts the positional parameters by two in order to correctly handle the next one. The only exception to this is the -a option which toggles on ARP for the interface. Because it has no argument, the option requires the script to
shift

by one parameter only. After reading all the options and their arguments, the script takes the last argument, which it expects to be the interface to be configured. Lastly, the script does its real work; namely, it sets the interface using the ifconfig command, and then it adds the network route. Processing options in this manner works but is rigid. The programmer must pay careful attention to the
shift

pattern in order to correctly handle options with arguments versus those without arguments. Another limitation is the fact that it does not handle stacked options. Although stacked options are not an absolute necessity, it is best to give the user the most flexibility with the command. The user can choose which method of specifying the arguments is most comfortable. By assuming that the arguments follow the options, the user does not gain this advantage. Luckily, there is a better way to deal with options. The shell provides a special utility specifically designed for processing command line options and their arguments called getopts. Its syntax is: getopts options variable. Getopts takes two arguments. The first is a list of valid command line options. If an option requires an argument, a scripter places a colon directly after the option. The second is a temporary variable used for parsing through the arguments. To use getopts, the programmer employs a loop. Each time getopts is called within the loop, it processes the next command line argument. If it finds a valid option, then the option is stored in the variable. If it encounters an invalid option, getopts stores a ?

68 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

inside variable for error handling. When an option takes an argument, getopts stores the associated argument in the special variable OPTARG. If it cannot locate the option's argument, getopts sets OPTARG to ?. From a user's point of view, a script using getopts follows one simple rule. Options must always be preceded by a dash. If listed separately, each option takes a dash. If stacked, the option list begins with a dash. This is required for getopts to differentiate between options and their arguments. It is hardly a burden for users to do this and will help beginners of UNIX to learn its syntax. Returning to the previous example, setether can be rewritten with getopts:
#!/bin/sh # # setether: set an Ethernet interface's IP configuration # while getopts ab:e:i:m:n: option do case "${option}" in a) ARP="arp" b) BROADCAST=${OPTARG};; e) INTERFACE=${OPTARG};; i) IPADDRESS=${OPTARG};; m) NETMASK=${OPTARG};; n) NETWORK=${OPTARG};; *) echo "setether: illegal option: ${option}" exit 1 ;; esac done ifconfig ${INTERFACE} ${IPADDRESS} netmask ${NETMASK} broadcast ${BROADCAST} ${ARP} route add -net ${NETWORK}

Now setether loops upon the results of getopts. The script lists the valid command line options in the string ab:i:m:n:. The colons trailing the last four arguments force getopts to find the options' arguments. Then when the case statement tests for the current option processed, the script reads the argument from OPTARG. Upon completion of processing the arguments, the program sets the interface's IP configuration. The new version improves upon the original in a number of ways. For one, it assumes no special order of the command line arguments. The script even paraterizes the target interface so that it can be specified in any position. The real advantage to all of this is that the program lets getopts do the work. The programmer need not be concerned with any special shifting patterns necessary to get to the next argument. Another advantage is that the options can be stacked. If the user so desires, the options can be listed together rather than separately with their arguments. Also if an option is missing its argument, getopts performs error checking to make sure that one is present. There is now some built-in exception handling that was not present before. In the origial version, it is quite possible that the user could have forgotten an argument. Then the script would have happily read the next option as the previous option's argument. This would have deleterious results on the script's operation because the shift pattern would be incorrect. Finally, the new version is compact. It is smaller in code size and much easier to read since it focuses on the task at hand, namely, the processing of command line arguments. The only disadvantage to getopts is the fact that it can only handle single character options. It will not properly process options that are words as some programs do. If a script has so many options that it runs out of characters to handle them, then the scripter should consider processing positional parameters. On the other hand, the scripter should also consider that the program being written could possibly use a redesign in order to make it easier to use.

69 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

8. Advanced Shell Topics


1. 2. 3. 4. 5. 6. 7.

More I/O Synchronicity Arithmetic Eval Statements Exec Statements Setting Shell Flags Signals and Traps

8.1 More I/O


Input and output techniques within the shell was first introduced in Section 1.4, Shell Basics: I/O. That section covered input and output redirection of stdin and stdout to files. By now, the reader should be fairly familiar with these techniques from having used them to write to and read from text files. This section expands upon these concepts by showing how to use the same constructs for redirection to file descriptors and by combining loops with I/O redirection. The shell permits the combination of the basic I/O constructs with a file descriptor. A file descriptor is nothing more than a non-negative digit that points at a file. File descriptors are also known as file handles. They are very useful for interprocess and network programming. The reader should not be terribly concerned if this concept is confusing. From the shell's perspective, only stdin, stdout, and stderr are truly important. Anything else probably requires a more powerful language such as C, which can make system calls to create and gain access to other file descriptors. The file descriptors for stdin, stdout, and stderr are 0, 1, and 2, respectively. Any of these may be redirected to a file or to each other. In other words, it is quite possible to send the output from stdin and stderr to the same file. This is quite useful when a user would rather check a script's results after it has completed processing. The methods for doing this are shown in Table 8.1-1. Table 8.1-1. Methods for File Descriptor Redirection Meaning Redirects descriptor's standard input to the same input that file uses. Redirects descriptor's standard output to the same output that file uses. Redirects descriptor's standard input to the same input that file uses. For use when standard input has already been redirected. Redirects descriptor's standard output to the same output that file uses. For use when standard output has already been redirected. Closes standard input Closes standard output

Construct
descriptor<file

or
descriptor<<file descriptor>file

or
descriptor>>file file<&descriptor

file>&descriptor <&>&-

Granted, this syntax looks strange, and in fact, it takes some practice to get them right. Below are some examples.
$ cd /bogus >>out /bogus: does not exist

70 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ cat out

The user starts by trying to change directories to a non-existant directory. In the process, the output from the command is redirected to the file out. Unfortunately, the user neglects that fact that the output generated by cd is an error and is output on stderr. So when the user displays the contents of out, it contains nothing. This time, the user tries redirecting stderr to the same file in which stdout is redirected.
$ cd /bogus >>out 2>&1 $ cat out /bogus: does not exist

This time, out contains the error message. The key is the addition of 2>&1. This is a very important concept, and to understand it, the line should be read, "Change d irectory to /bogus, redirect stdout to the file out, and then redirect stderr to the same file that stdout uses." The last part is exactly what 2>&1 means. Moreover, most scripters find that the 2>&1 construct is the most widely if not the only of these constructs used. As stated previously, it is extremely useful for saving the output to a file for reviewing after the script completes. For example, a backup script that runs each night might do this and then email the output to the system administrator. As a quick note, the same results could have been generated simply by preceding the output redirection with stderr's file descriptor.
$ cd /bogus 2>>out $ cat out /bogus: does not exist

As the last example of advanced redirection, the results of printing the working directory are suppressed by closing stdout.
$ pwd >&$

Now using these within a script can become a bit tiresome. A programmer needs to precede each command that could produce output or errors with appropriate redirection. Actually, to get around this, the user just executes the script at the command line with the same redirection. But if the scripter chooses to do the work for the user, then the shell provides a method for redirecting all of a script's input and output. A form of the exec command allows this: exec construct file, where construct is a redirection and file is the input or output to use. For example,
#!/bin/sh exec >>${0}.out

redirects the output for the script into the file given by ${0}.out. ${0}.out is the new file named the same as the script but with .out appended. Next, redirection can be used with loops. To do so, the programmer adds the redirection construct to the end of the loop declaration. In fact, loops can use redirection, piping, and even backgrounding by the same method, though redirection is easily the most useful. Below is a short script demonstrating the technique by printing the disk usage of each user on a system.
$ USERS=`cat /etc/passwd | cut -f1 -d:` $ for user in ${USERS} > do > du -sk /home/${user} > done >>diskusage 2>/dev/null $ cat diskusage 2748 /home/ftp 38931 /home/jac 109 /home/mlambi

71 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

26151 5 8185 13759

/home/rsayle /home/bobt /home/mann_al /home/bheintz

The script starts by getting a list of users from the /etc/passwd file. It stores the list in the variable USERS. Then it parses through the list with a for loop. Each iteration of the loop performs a disk usage summary on the current user's home directory. The results are in turn saved in the file diskusage by redirecting standard output into it. Standard error is sent to the great bit bucket in the sky because some accounts listed in /etc/passwd do not actually have home directories. This would cause du to complain that /home/${user} for the current user does not exist. The results are then displayed by catting diskusage. Standard input works well with loops. As it turns out, redirecting stdin with a loop is the way one can read a file line by line in order to process its contents. Combining this with a little output redirection creates a slick way to generate files from a template and a list of data to fill the template.
$ cat mkraddb #!/bin/sh # # mkraddb: generate the RADIUS configuration of a set of users # # # Global Vars PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} users_file" DATE=`date +%m%d%y` RADDBUSERS="raddb.users.${DATE}" ############################################## # buildRaddbUsers ############################################## buildRaddbUsers () { cat >>${RADDBUSERS} <<EOF ${name}-out Password = "ascend", User-Service = Dialout-Framed-User User-Name = ${name}, Framed-Protocol = MPP, Framed-Address = ${ipaddr}, Framed-Netmask = 255.255.255.255, Ascend-Metric = 2, Framed-Routing = None, Ascend-Route-IP = Route-IP-Yes, Ascend-Idle-Limit = 30, Ascend-Send-Auth = Send-Auth-CHAP, Ascend-Send-Passwd = "password", Ascend-Receive-Secret = "password" EOF }

############################################## # Main ##############################################

72 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

if [ $# -ne 1 ]; then echo ${USAGE} exit 1 fi # Create the configs while read name ipaddr do buildRaddbUsers done <${1} # Clean up exit 0

So here is a script called mkraddb. Its sole purpose is to read a file and generate the RADIUS profiles for the users listed in the file. Focusing on the redirection within the script, the program first uses redirection with the while loop under the main program. The loop reads from the file passed to the script as its argument. The redirection of the first positional parameter into the while loop does this. The read command following the
while

serves as its test. It takes each line from the file and reads the line's contents into the temporary variables name and ipaddr. As long as there are lines in the file to read, read returns true and the loop executes. When there are no more entires left, the loop terminates and, consequently, so does the script. To process the data, the loop calls the function buildRaddbUsers. Turning back to this function shows the second usage of redirection within the script. Input redirection causes the function to create a new file as given by the value of RADDBUSERS. The output redirection of cat instructs the script to write into this file, the text between the end of the file markers, EOF. The shell expands the variables name and ipaddr as it writes the text to create the user's RADIUS profile. Here is how it works.
$ ls mkraddb users $ cat users bob 192.168.0.1 joe 172.16.20.2 mary 10.0.25.3

The user first gets a directory listing that shows only the script, mkraddb, and the users file. Then the operator cats users and notes that the script should create RADIUS entries for bob, joe, and mary. Below, the operator executes mkraddb passing it users. A quick listing shows that mkraddb created raddb.users.082698, and inspecting the contents of the new file shows the profiles for these users.
$ mkraddb users $ ls mkraddb raddb.users.082698 users $ cat raddb.users.082698 bob-out Password = "ascend", User-Service = Dialout-Framed-User User-Name = bob, Framed-Protocol = MPP, Framed-Address = 192.168.0.1, Framed-Netmask = 255.255.255.255, Ascend-Metric = 2, Framed-Routing = None, Ascend-Route-IP = Route-IP-Yes, Ascend-Idle-Limit = 30, Ascend-Send-Auth = Send-Auth-CHAP, Ascend-Send-Passwd = "password",

73 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Ascend-Receive-Secret = "password" joe-out Password = "ascend", User-Service = Dialout-Framed-User User-Name = joe, Framed-Protocol = MPP, Framed-Address = 172.16.20.2, Framed-Netmask = 255.255.255.255, Ascend-Metric = 2, Framed-Routing = None, Ascend-Route-IP = Route-IP-Yes, Ascend-Idle-Limit = 30, Ascend-Send-Auth = Send-Auth-CHAP, Ascend-Send-Passwd = "password", Ascend-Receive-Secret = "password" mary-out Password = "ascend", User-Service = Dialout-Framed-User User-Name = mary, Framed-Protocol = MPP, Framed-Address = 10.0.25.3, Framed-Netmask = 255.255.255.255, Ascend-Metric = 2, Framed-Routing = None, Ascend-Route-IP = Route-IP-Yes, Ascend-Idle-Limit = 30, Ascend-Send-Auth = Send-Auth-CHAP, Ascend-Send-Passwd = "password", Ascend-Receive-Secret = "password"

8.2 Synchronicity
Due to UNIX's multitasking behavior, commands can be sent to the background so that they are executed when the kernel finds time. After backgrounding a command, the shell frees itself to handle other commands. In other words, while the backgrounded command runs, the operator's shell is available to execute other commands. To background a command, the user appends an ampersand, &, to it. Backgrounded commands are also referred to as jobs. Searching through a rather large hard disk for a file is a common candidate for backgrounding.
$ find / -name samunix.tar -print 2>/dev/null & [1] 10950 $ cd proj/book $ mkdir advanced $ cd advanced /home/rsayle/proj/ucb/samunix.tar $ touch example.sh [1]+ Exit 1 find / -name samunix.tar -print 2>/dev/null $

In this case, the operator decides to search for a missing tar file. The user runs the find command and instructs the program to start the search from the root directory. Any errors produced by find, namely directories that find is unable to search due to illegal priviliges for the user, are redirected to /dev/null. This way, the user can continue to do work without being interrupted. The user backgrounds the command as well. The shell returns a message indicating that the command is running with process identifier 10950. If the user chooses, a kill signal can be sent to the command to end it prematurely. While the find runs, the operator continues with work. The operator changes the working directory, creates a new subdirectory, and

74 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

then changes working directories once more into the newly created subdirectory. After this last action, the find command returns a successful hit. It found the tar file in /home/rsayle/proj/ucb. The user continues by creating a new shell script called example.sh. The backgrounded job finishes its processing just after this, and it returns a message stating that it is complete. Sometimes it may be necessary to synchronize a script with an asynchronous process, ie. a backgrounded job. The shell provides the wait built-in command for such synchronization. Wait takes an optional process number as its argument. It pauses the shell's execution until the specified process had terminated. If only one process has been sent to the background, then wait can be issued without an argument. It will automatically wait for the job to complete. If more than one job is running, it becomes necessary to store their process identifiers in variables and pass the appropriate job number to wait.
#!/bin/sh # # stopOV: stop HP OpenView # PROG=`basename ${0}` # check for the root user if [ "${USER}" != "root" ]; then echo "${PROG}: only the root user can halt OpenView." exit 1 fi # stop OpenView using its utility ovstop >>ovstop.$$ 2>&1 & echo "${PROG}: attempting to stop all OpenView processes..." wait # check for runaway OpenView processes STILLRUNNING=`grep "RUNNING" ovstop.$$` if [ "${STILLRUNNING}" != "" ]; then for ovprocess in ${STILLRUNNING} do ovpid=`echo ${ovprocess} | cut -f1 -d" "` echo "${PROG}: killing OpenView process ${ovpid}" kill ${ovpid} & LASTOVPID=$! done fi # wait for the last one to die wait ${LASTOVPID} # notify administrator if there are still run away processes for ovproc in ${STILLRUNNING} do runaway=`ps -ef | grep ${ovproc}` if [ "${runaway}" != "" ]; then echo "${PROG}: could not stop OpenView process: ${runaway}" fi done # Clean up exit 0

The script aboves uses both forms of the wait command. StopOV is a shell script that halts the HP OpenViewTM

75 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

network management platform. When the root user runs it, the script first tries using the stop utility that comes with the software. It runs ovstop sending its output to the file ovstop.$$. If all goes well, then this file will be empty. The ovstop, as issued by the script also redirects stderr to the same file. If any OpenView processes fail to terminate, it lists them in ovstop.$$ beginning with their process identifier. The script sends the command to the background, prints a friendly message indicating that it is working, and then waits. Here the script uses wait without any arguments since ovstop is the only job. Upon completion of ovstop, the script continues by checking ovstop.$$ for any errors. If it finds any processes still running, as stored in the STILLRUNNING variable, then it tries to halt them with the kill command. To do this, the script employs a for loop. It parses through the contents of STILLRUNNING and picks out the errant processes. It sends a kill signal to these processes and backgrounds each attempt. Through each iteration, it stores the attempt in LASTOVPID. The final iteration of the loop resets LASTOVPID to the last kill issued. The script then waits for the last kill to complete. This of course assumes that all other kills issued complete prior to the last one finishing. This may be an errant assumption, but as it turns out, it is sufficient for this program because when the last terminates, the scripts continues by checking the process table for any run away processes. StopOV shows this in the last for loop. If it finds any, it notifies the OpenView administrator so that the manager can take further action.
kill

8.3 Arithmetic
Eventhough the shell treats all values as strings, a built-in command called expr interprets numeric strings as their integral values. Moreover, it understands how to perform basic arithmetic including addition ( + and -) as well as multiplication (* and /). Actually, expr is much more feature rich than just a simple calculator. Expr can also handle pattern matching, substring evaluation, and logical relations (e.g. greater than comparisons). Still, for these functions, UNIX provides tools that are much better suited to these tasks such as grep, sed, and awk for pattern matching and the built-in logical operators for comparisons. In any event, there are moments when a script calls for basic mathematics. A good example is when a programmer needs to increment a counter variable for a loop. The expr command suits such a task well. As an example of how to use expr, the script below demonstrates the calculation of the Pythagorean Theorem.
$ cat csquared #!/bin/sh # # csquared: almost calculate the hypotenuse of a rt triangle # PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} base height" if [ $# -ne 2 ]; then echo ${USAGE} exit 1 fi ASQUARED=`expr ${1} \* ${1}` BSQUARED=`expr ${2} \* ${2}` expr ${ASQUARED} + ${BSQUARED} $ csquared 3 4 25

76 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

Because expr cannot handle complex math such as exponentials, the script breaks the calculation into three steps. It evaluates the squares of each side of the right triangle and stores the results in the temporary variables ASQUARED and BSQUARED. During the evaluation of each expr, the programmer must escape the multiplication symbol so that the shell does not expand it as a wildcard. The final calculation, which is the sum of the squares, is handled directly by expr. It returns the result to the user's screen as demonstrated by the invokation of csquared. UNIX has a much more precise and powerful calculator called bc. It can evaluate real valued numbers, arrays, and exponentials, just to name a few. Bc utilizes an interactive interface that reads from stdin and writes to stdout, but it also supports reading from and writing to files. This fact makes it useful for shell scripts. Now csquared can be rewritten with bc making it a much more compat program.
$ cat csquared #!/bin/sh # # csquared: almost calculate the hypotenuse of a rt triangle # PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} base height" if [ $# -ne 2 ]; then echo ${USAGE} exit 1 fi bc <<EOF ${1}^2 + ${2}^2 quit EOF $ csquared 3 4 25

This version of csquared does not need intermediate variables since bc can evaluate complex expressions. To pass the calculation to bc, the script employs input redirection, and to pass it the values for the expression, the shell uses the positional parameters. Further, the calculation is handled in one line as the theorem itself is expressed, a2 + b2. Bc prints the results on the user's terminal, but it could also easily be redirected to a file if needed.

8.4 Eval Statements


Eval

is a tricky beast. This built-in command instructs the shell to first evaluate the arguments and then to execute the result. In other words, it performs any file expansion or variable substitution, then it executes the command. During the execution, the shell, because of its nature, once again does an expansion and substitution and then returns a result. Essentially, eval is a double scan of the command line. This is useful for command in which the shell does not process special constructs due to quoting.
$ spool=/var/spool $ ptr='$spool' $ echo; echo $ptr; echo $spool

77 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

$ eval echo $ptr /var/spool

The example above demonstrates the double scan by creating a pointer variable. First, the operator creates the variable spool and stores the directory /var/spool into it. Next, the user declares a pointer, ptr, and stores in this variable $spool. The single quotes prevent the shell from evaluating $spool; hence, ptr does not contain spool's value. To show the value of spool, ptr's contents are echoed to the screen. Additional echos help to differentiate the value from other commands. Then, to get back the value ptr points to, the user employs eval. When the shell invokes eval, it scans the arguments that follow and does a variable substitution on ptr. This leaves the shell with echo $spool. After this, the shell processes this command. It performs another scan and expands $spool into its contents, /var/spool. With the second scan complete, the shell prints /var/spool to the terminal.

8.5 Exec Statements


The exec built-in command spawns its arguments into a new process in lieu of the currently running shell. Its syntax is relatively simple: exec command args. When issued, the invoking shell terminates, and command takes over. There is no possibility of returning. Command runs as if it was entered from its own prompt. The function below shows how a script might use exec to run another program after it is complete.
execute_custom_script () { if [ "${CUSTOMSCRIPT}" = "" ]; then exit else if [ -x ${CUSTOMSCRIPT} ]; then echo "Executing ${CUSTOMSCRIPT}" exec ${CUSTOMSCRIPT} fi fi }

8.6 Setting Shell Flags


Set

is another built-in shell command. Its primary function allows the toggling of shell options for controlling a script's behavior. The list of options is given in the table below. Options are enabled by preceding them with a dash (-) and are disabled when preceded with a plus ( +). Options can be stacked. When used as the first command issued in a script, set and its options can be a very useful debuggin tool. Table 8.6-1. Options to the set command Option Meaning
--a -e -f -h

Do not treat the subsequent arguments as options Automatically export all variable defined or modified Exit if any command executed has a nonzero exit status Disable file name expansion Remember the location of commands used in functions when the functions are defined (same as the hash command) Process arguments of the form keyword=value that appear anywhere on the command line and place them in the environment command

-k

78 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

-n -t -u -v -x

Read commands without executing them Exit after executing one command Issue an error if a null variable is referenced including positional parameters Print each shell command line as it is read Print each command and its arguments as it is executed with a preceding +

Set provides a second function for scripts. It allows a user to change the positional parameters by giving a list of words as arguments. This can be useful for changing the arguments a script or function should process, but in practice, it is used rarely. Still, to demonstrate, the commands below show how to do this. The shell, as it is normally invoked in an interactive session, does not have any arguments passed to it. So the operator changes this by executing set with a well-known phrase. A while loop processes the new positional parameters by printing them on the console.
$ set goodbye cruel world $ while [ $# -gt 0] > do > echo ${1} > shift > done goodbye cruel world

8.7 Signals and Traps


UNIX uses a mechanism called signalling to send messages to running processes directing them to change their state. A signal is anumber that has a predefined meaning. There are about 30 signals available. The most commonly used signals are listed below. Signals may be sent by using the kill command or sometimes by entering special control characters (Ctrl-c, Ctrl-z) at the terminal where the program is running. Table 8-7.1. Common UNIX Signals Signal Name Description Default Action Catchable Blockable 1 2 9 11 15 17 19 SIGHUP SIGINT SIGKILL SIGSEGV SIGTERM SIGSTOP SIGCONT Hangup Interrupt Kill Segmentation violation Software termination Stop Continue after stop Terminate Terminate Terminate Terminate Terminate Stop Ignore Yes Yes No Yes Yes No Yes Yes Yes No Yes Yes No No

As shown in the table above, some signals can be caught. In other words, a program can detect the signal and then execute some action predefined by the scripter. Handling signals allows scripts to gracefully terminate. To catch a signal, a script uses the trap command. Trap has a few different acceptable forms, but the general syntax is: trap commands signals. In this form, trap instructs the shell to catch the signals listed by their numbers in the list of signals. The shell handles the signals by executing commands, a list of valid UNIX commands. To demonstrate, handletraps catches SIGINT and SIGTERM.
$ cat handletraps #!/bin/sh OUTPUT=out.$$

79 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

trap "if [ -f ${OUTPUT} ]; then rm ${OUTPUT}; exit; fi" 2 15 touch ${OUTPUT} while true do sleep 5 done $ handletraps & [1] 836 $ ls handletraps out.836 $ kill -15 836 $ ls handletraps

The program above starts by defining a variable whose value is out.$$. It then sets up the signal handling with a trap. The trap catches signals 2 and 15. When these signals are sent to handletraps, the program checks for the existence of out.$$. If it is present, it removes the file and then terminates. The script continues by touching the file and then sits in an endless loop. The user executes handletraps sending it to the background. The shell shows it is running as process number 836. A quick directory listing shows that the directory now contains the script plus the touched file, out.836. The operator then sends a SIGTERM to handletraps with the kill command. Another directory listing shows that the script caught the signal and ran the if block which removed out.836. In its other forms, trap can be issued with no arguments. This second form of trap just displays the list of currently handled signals. In its third form, signals can be reset. To do this, the programmer omits the set of commands from trap but provides a list of signals. This resets the signals listed to their defaul behavior. In its fourth form, trap can be used to ignore a set of signals. This is done by giving an empty command list, denoted by "", to trap. The last form of trap uses a colon as the command list. This causes the parent shell to ignore a signal but its subshells terminate if they receive any signals listed in signals. A couple properties of the trap command are important to remember. Subshells inherit the trap behavior of their parents. Also, the shell scans command once when it encounters the trap and then again when it processes a signal; consequently, it may be necessary to use single quotes to prevent expansion until the signal is caught.

9. Debugging Hints
Declaring the Shell
A very common mistake among novice scripters is that they forget to declare the shell. The first line of a shell script should always be the shell declaration, #!/bin/sh. Without it, the script automatically inherits the shell of the parent program. In other words, when a script does not declare the shell type, it uses the same shell that is currently being used. This can yield undesirable results if, for example, an operator uses the csh for command line processing and then decides to run a Bourne shell script that does not reset the running shell. The results of neglecting to declare the shell are not necessarily deleterious. In most cases in fact, it

80 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

simply results in a program crash with a lot of unexpected errors. But it happens often enough to UNIX users that it is worthy of mention and should be added to the list of debugging hints.

Tracing Program Execution


Traditional programming languages that require compiling and linking of programs have sophisticated tools for debugging. These tools require a program to be compiled with certain flags that allow the program's symbols and instructions to be loaded into a run-time debugger. Within the debugger, a programmer can do all sorts of nifty things such as step through a program's execution, set stop points, dump the function stack, and even examine and sometimes change the state of variables. Of course, all of these actions allow the programmer to understand, correct, and fine tune the program's execution. Unfortunately, the shell does not have a readily available debugger, but it does have a tool that is close enough. By providing options to the set command a programmer can trace the steps through a script's execution. All of the options to set were introduced in Section 8.6, Setting Shell Flags. The most useful option is -x. This option causes the shell to print each command as it is evaluated. This is especially useful to a scripter because the results of actions such as variable substitution and file name expansion are shown. Set -x could be further enhanced by combining -v with -x. The -v option instructs the shell to print a command before it is evaluated. The combination obviously allows a scripter to see the command followed by the results of the shell evaluating it. In practice, however, this results in an extremely verbose output that may actually be difficult to follow. Readers are encouraged to try both methods and decide which works for the job at hand. The rest of this chapter will only consider the use of -x. Now to turn the tracing option on, a script can be executed at the command line by preceding it with sh -x, but a better method is to include the set -x command in the script. Placing this command on a line by itself instructs the shell to start tracing on the commands that follow. To disable it, the scripter simply comments out the line. It is a good habit to include this line within scripts so that debugging is readily available. As an example, the mkraddb script is reintroduced. Mkraddb takes a plain text file as its argument and generates a RADIUS database from its contents:
#!/bin/sh # # mkraddb: generate the RADIUS configuration of a set of users # # # Uncomment the following line for debugging set -x # Global Vars PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} users_file" DATE=`date +%m%d%y` RADDBUSERS="raddb.users.${DATE}" ###################################################################### # buildRaddbUsers ###################################################################### buildRaddbUsers () { cat >>${RADDBUSERS} <<EOF ${name}-out Password = "ascend",

81 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

User-Service = Dialout-Framed-User User-Name = ${name}, Framed-Protocol = MPP, Framed-Address = ${ipaddr}, Framed-Netmask = 255.255.255.255, Ascend-Metric = 2, Framed-Routing = None, Ascend-Route-IP = Route-IP-Yes, Ascend-Idle-Limit = 30, Ascend-Send-Auth = Send-Auth-CHAP, Ascend-Send-Passwd = "password", Ascend-Receive-Secret = "password" EOF }

###################################################################### # Main ###################################################################### if [ $# -ne 1 ]; then echo ${USAGE} exit 1 fi # Create the configs while read name ipaddr do if [ "${name}" = "#" -o "${name}" = "" ]; then # continue if the line is a comment or is blank continue fi # Create the RADIUS users information buildRaddbUsers done <${1} # Clean up exit 0

The eighth line is particularly important because it demonstrates the use of set -x. The script lists this command prior to any others. By doing so, it enables tracing on the rest of the script. An astute reader might decide that rather than making it the first command, it could be enabled by making it an option to the program. For example, entering mkraddb -d at the command line might cause the script to dynamically execute tracing. This in fact can be done, but it should be noted that it would also disable the debugging of option handling itself. For the example, a programmer runs mkraddb with tracing enabled. The script is passed the file users whose contents are first displayed for comparing with the trace output. The trace then follows. Line numbers are included with the trace for reference during the explanation of the results; line numbers are not part of the output when a script is run with set -x. The directory listing is also shown at the end simply to demonstrate successful creation of the RADIUS profiles.
$ ls mkraddb users $ cat users bob 192.168.0.1

82 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

joe 172.16.20.2 mary 10.0.25.3 $ mkraddb users 1 ++ basename ./mkraddb 2 + PROG=mkraddb 3 + USAGE=mkraddb: usage: mkraddb users_file 4 ++ date +%m%d%y 5 + DATE=090598 6 + RADDBUSERS=raddb.users.090598 7 + [ 1 -ne 1 ] 8 + read name ipaddr 9 + [ bob = # -o bob = ] 10 + buildRaddbUsers 11 + cat 12 + read name ipaddr 13 + [ joe = # -o joe = ] 14 + buildRaddbUsers 15 + cat 16 + read name ipaddr 17 + [ mary = # -o mary = ] 18 + buildRaddbUsers 19 + cat 20 + read name ipaddr 21 + exit 0 $ ls mkraddb raddb.users.090598 users

The trace starts right where the program sets its global variables. Lines one through six show the process. Referring back to the script's code above, the first variable is PROG, which stores the script's name. To set PROG's value, the basename command is run against the zeroeth positional parameter. The trace's first line shows this action. One interesting item to note is the expansion of ${0} into ./mkconfig. The second line displays PROG being set to the results of the first action. Then line three shows USAGE being set. USAGE gets a string that explains how to properly execute the program. The string's value uses PROG to help create the message as can be seen by comparing the code versus line three's output. Lines four through six demonstrate much of the same. Four and five show that DATE gets assigned the result of executing the date command, and six then uses DATE's value to generate the name of the script's resultant file stored in RADDBUSERS. Line seven displays the trace of the next code block. After setting the global variables, the script then checks that it is executed properly. This is done with the if block that immediately follows the comment that marks where the main program execution begins. The if blocks tests the command line options to be certain that an argument was passed to the script. Line seven shows the evaluation of the if's test, which checks to see if $# is not equal to one. If it is not, the script prints the USAGE error message and terminates so that the user can correct the problem. In this instance, $# in fact evaluates to one making the test false. Since it is false, the user ran the program correctly, and the script proceeds past the if block into its real work. The rest of the example shows what a loop looks like when being traced. It can effectively be seen in blocks of four lines each. Lines eight through eleven are the first execution of the script's while loop. The loop keys off of the line by line processing of the file passed as the script's first argument. As long as their is a non-empty line in the file, the loop stuffs the line's value into the intermediate variables name and ipaddr. This is exactly what line eight shows. The read command reads from users, finds bob and bob's IP address, stuffs these two values into name and ipaddr, and then returns true to while. Since while receives a true value for its test, the loop proceeds. The ninth line displays the if block contained within the while loop. This block checks to see if a comment or blank line was read from the file. It does so by comparing the value of name against the strings # and "",
83 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

the null string. As can be seen on line nine, bob is definitely not the same as either of these test values, and so the program moves forward. Its next step is to call the function buildRaddbUsers as is done on line 10. Line eleven is the complete trace of the function's entire execution. All the trace shows is that the script calls the cat command. This is a far cry from what the function is really doing. Looking at the function's code shows that it uses cat in a complex redirection scheme to create the user's profile. To be more specific, it appends the lines between the end of file markers, EOF, into the RADDBUSERS file. During this process, it substitutes the values of the intermediate variables name and ipaddr in their appropriate places within the user's RADIUS profile. But all of this action is hidden within the trace as simply an execution of cat. The trace then shows the next interations of the loop. To summarize, the loop performs the same process on the next two lines in users. After adding the two other users listed in the file, the loop executes one last read at line 20 of the trace. The final read returns a false value to the loop since there are no more lines to read from users. At this point, the loop terminates and picks up at the script's exit point shown by line 21. As a final note, scripters are reminded to recomment the set -x statement after performing debugging. It is very easy to do all sorts of work to get a program to run properly and then to forget to turn off the trace. Needless to say, it could be quite confusing to users who then execute the script and get all sorts of output that is really nothing more than gibberish to the untrained eye.

Command Line Debugging


One of the primary advantages of shell scripting over traditional programming languages is the ability to run the same commands given in a script at the command line. Any command in a shell script can be run at the UNIX command prompt and vice versa. This feature makes the shell good for rapid prototyping because a programmer can try constructs in a live environment before commiting them to a script. By the same token, this makes the command line a useful debugging tool. If a scripter suspects a portion of a script to be causing an error, the programmer may run the command block at the UNIX prompt. It can be done so in a controlled manner. A scripter can preset variables or functions and then issue commands one at a time. After each step, the scripter has the opportunity to check the state of variables and examine the results of the previously issued command. There are many examples given in this book that use the command line to demonstrate programming techniques. Rather than show yet another, this subsection finishes by listing some good cases of when this technique is useful. Programmers can use command line debugging: To check a variable's value when it is being set by file name expansion or command substitution. To build filters step-by-step. For building or testing if statements, case blocks, and loops. To determine all the necessary options to a command before entering it in a script. For testing a command block to identify where an error is occurring.

Pausing Program Execution


As stated earlier in this chapter, one of the features of a debugger is the ability to set stop points in a program. When a user loads the program into the debugger and runs it, the debugger pauses the program at each stop point. With the program stopped, the user gets the opportunity to check the program's state and understand how well it is functioning.

84 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

The shell does not have a debugger to do the same thing, but it can be emulated by a combination of using set -x and the strategic placement of read statements in the script. Set -x causes the shell to print each command as it executes them from a script, and a read statement forces the program to stop and await user input. Since the program stops at the read command, the programmer gets a chance to review the script's execution up to that point. Hence, a read acts like a stop point. When the user is ready to continue to the next stop point, a bogus value can be entered at the prompt. If no argument follows the read command, the shell simply discards the bogus entry. Actually, no value needs to be provided. Hitting enter suffices. If a programmer solves the problem being investigated, the user may alternatively terminate the program's execution by entering a control character. To demonstrate, here is a script that counts to five:
$ cat count #!/bin/sh set -x COUNT=0 while [ ${COUNT} -le 5 ] do echo "COUNT=${COUNT}" read COUNT=`expr ${COUNT} + 1` done

Needless to say, this is hardly a complex script, but it shows the technique well enough. The first thing to notice is that the script enables tracing immediately with set -x. The second is the placement of read for pausing the script. The programmer chooses to embed it within the loop. In fact it halts the program just before it changes COUNT's value. Presumably, the programmer wants to watch COUNT by checking its value before attempting to increment it. Executing the script shows the results.
$ count + COUNT=0 + [ 0 -le 5 ] + echo COUNT=0 COUNT=0 + read ++ expr 0 + 1 + COUNT=1 + [ 1 -le 5 ] + echo COUNT=1 COUNT=1 + read ++ expr 1 + 1 + COUNT=2 + [ 2 -le 5 ] + echo COUNT=2 COUNT=2 + read ++ expr 2 + 1 + COUNT=3 + [ 3 -le 5 ] + echo COUNT=3 COUNT=3 + read ++ expr 3 + 1

85 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

+ COUNT=4 + [ 4 -le 5 ] + echo COUNT=4 COUNT=4 + read ++ expr 4 + 1 + COUNT=5 + [ 5 -le 5 ] + echo COUNT=5 COUNT=5 + read ++ expr 5 + 1 + COUNT=6 + [ 6 -le 5 ]

In the first block above, the trace shows COUNT being set to its initial value of zero. Following that is the first test of the variable in the while loop. The test passes, so the script prints the the variable's value shown by the echo and the resulting output. The block ends with a read. At that point, the shell pauses execution pending input from an operator. Now these actions happen much faster than it takes to read this paragraph and compare it against the text above, but by using the read to stop the script, the operator has a chance to review it. After doing so, the operator hits the enter key shown by the blank line. The program reads the return character entered and resumes its execution. The next trace block is much the same as the first except that it shows the incrementing of COUNT. First, the expr command adds one to the current value of COUNT, and then the shell assigns the new value to the variable. The loop tests the new value, still finds that it is within range, prints it, pauses at the read once more, and awaits user input. The process repeats until the variable's value passes five and then the script completes normally.

Null Commands
Sometimes, a good way to debug a script is to do nothing at all. The shell provides a null command in the form of a colon. This command can be useful for checking the various tests in an if block, the switches of a case, or a loop's exit condition. The idea is that a programmer can write the framing of an if, case, or loop but provide the null command for the internal processing. Then the script can be run to allow the testing of the code without sweating the details of the its real function. To demonstrate, below is the beginning of a script to check the disk usage for a set of user accounts.
$ cat ducheck #!/bin/sh PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} name1 ... nameN" if [ $# -gt 0 ]; then : else echo ${USAGE} exit 1 fi

The script's author currently has only written the code for verifying that the correct number of arguments are passed to the program. If they are, then the script executes the null command, breaks out of the if block,

86 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

and terminates with no output. Eventually, when the programmer determines the command to gather the disk usage, the user can replace the colon with the appropriate functions. A test run of ducheck shows that the script behaves accordingly when it is passed arguments.
$ ducheck root securid rsayle $

But for the time being the scripter wishes only to test the proper execution of the if statement when the arguments to the script are incorrect. In particular, the author wants to check that the script prints an error message when no arguments have been passed.
$ ducheck ducheck: usage: ducheck name1 ... nameN $

As can be seen, the script handles an empty argument list just as the programmer intended.

Interpretting Errors
To conclude this book, the final section shows examples of syntax errors and how the shell reports them. The reader should note that due to variations in UNIX, the specific error messages may not be exactly the same. Still, the examples do provide some insight into common mistakes, and hopefully, the wary scripter can learn them and identify them during debugging. The examples focus on a script called tgzit. The script's intended function is to take a list of files and directories as arguments and then to store them into a compressed tape archive. First, the correct script is shown for comparison against the errant versions.
#!/bin/sh # global variables PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} archive file1 ... fileN" # check the command line arguments if [ $# -lt 2 ]; then echo ${USAGE} exit 1 fi # get the archive name from the command line ARCHIVE=${1} shift # build the archive tar cf ${ARCHIVE}.tar "$@" gzip ${ARCHIVE}.tar mv ${ARCHIVE}.tar.gz ${ARCHIVE}.tgz

By this point, readers should be well versed in shell scripting and should be able to decipher the actions above. For the first example, our scripter tries running a slightly different version of tgzit and gets the following error message.
$ tgzit program-archive ./bin ./include ./source ./tgzit: syntax error near unexpected token `fi' ./tgzit: ./tgzit: line 11: `fi'

The shell is specific in the problem here. It states that it encountered the termination of an if block

87 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

somewhere near line 11 within the script. This at least points the user at a particular code block. Checking the contents of the script shows the error.
$ cat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -n tgzit #!/bin/sh # global variables PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} archive file1 ... fileN" # check the command line arguments if [ $# -lt 2 ] then echo ${USAGE} exit 1 fi # get the archive name from the command line ARCHIVE=${1} shift # build the archive tar cf ${ARCHIVE}.tar "$@" gzip ${ARCHIVE}.tar mv ${ARCHIVE}.tar.gz ${ARCHIVE}.tgz

At the eleventh line is the fi statement. Now this is not the error, and it requires a review of the entire if block to determine the problem. In this case, the error is fairly subtle. Looking back at the test condition on line eight, a careful examination reveals that the scripter failed to terminate the test statement with a semi-colon; hence, the complaint of the unexpected token. The fi was unexpected because the test was not punctuated correctly. This example not only demonstrates this common typo, but it also emphasizes that the shell can detect and report syntax errors, but it does not necessarily report the error's exact location. Programmers must remember to consider code blocks rather than single lines in order to find problems. Another common error involves misquoting. Here, a scripter executes tgzit and receives an error indicating this condition.
$ tgzit program-archive ./bin ./include ./source ./tgzit: unexpected EOF while looking for `"' ./tgzit: ./tgzit: line 20: syntax error

The interesting point to note is that the shell reports the correct problem, but it gives a false indication of where to find the error. Reviewing the source for this buggy version of tgzit, line 20 happens to be the final line in the script and, in fact, has no quotes whatsoever.
$ cat -n tgzit 1 #!/bin/sh 2 3 # global variables 4 PROG=`basename ${0}` 5 USAGE="${PROG}: usage: ${PROG} archive file1 ... fileN 6 7 # check the command line arguments 8 if [ $# -lt 2 ]; then 9 echo ${USAGE} 10 exit 1 11 fi

88 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

12 13 14 15 16 17 18 19 20

# get the archive name from the command line ARCHIVE=${1} shift # build the archive tar cf ${ARCHIVE}.tar "$@" gzip ${ARCHIVE}.tar mv ${ARCHIVE}.tar.gz ${ARCHIVE}.tgz

Quoting errors can be extremely difficult to find in especially large scripts because the shell does not pin-point the error's location. Fortunately, for this script, it is easy to see that at the end of the fifth line, the scripter forgot to close the double-quotes that set the USAGE variable. Still the shell does not report the error until after reading the entire script because it is perfectly legal for quoted strings to extend over multiple lines. Unfortunately for scripters this fact makes it hard to find such errors. They must either inspect a script by eye or hopefully can employ a command in their text editor that shows matching opening and closing punctuation. The next example is not really a syntax error as much as it is logical, but it is a frequent enough occurrence that it warrants attention. In this case, the programmer forgets that the proper usage of the script calls for at least two arguments: the first being the archive in which to store files and the rest being the files to archive.
$ cat tgzit #!/bin/sh # global variables PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} archive file1 ... fileN" # check the command line arguments if [ $# -lt 1 ]; then echo ${USAGE} exit 1 fi # get the archive name from the command line ARCHIVE=${1} shift # build the archive tar cf ${ARCHIVE}.tar "$@" gzip ${ARCHIVE}.tar mv ${ARCHIVE}.tar.gz ${ARCHIVE}.tgz

The problem is in the test for the correct number of arguments. The scripter incorrectly checks for at least one argument. The program assumes that the first argument is the archive to be built. If a user executes the tgzit with just one argument, tar has nothing to put in the archive.
$ tgzit program-archive tar: Cowardly refusing to create an empty archive Try `tar --help' for more information. prog-archive.tar: No such file or directory mv: prog-archive.tar.gz: No such file or directory

The script itself does not yield the error. Instead, the tar command notes that it has nothing to do. The script continues past the tar statement, but the remaining commands also have nothing to do since the archive does not exist. They complain accordingly. Scripters must be careful to check and test their scripts' arguments. Another common error scripters make is simply a typo. It is very easy to forget to close punctuation

89 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

properly or to create malformed statements all together.


$ cat tgzit #!/bin/sh # global variables PROG=`basename ${0}` USAGE="${PROG}: usage: ${PROG} archive file1 ... fileN" # check the command line arguments if [ $# -lt 2]; then echo ${USAGE} exit 1 fi # get the archive name from the command line ARCHIVE=${1} shift # build the archive tar cf ${ARCHIVE}.tar "$@" gzip ${ARCHIVE}.tar mv ${ARCHIVE}.tar.gz ${ARCHIVE}.tgz

The syntax error here is quite subtle. The shell, however, still catches it.
$ tgzit program-archive ./bin ./include ./source ./tgzit: [: missing `]'

The shell reports that there is a problem with a test. Luckily, for this script, there is only one test to examine; namely, the if block that checks the program's arguments. The scripter forgot to place a space between the two and the closing square bracket. Had the script contained multiple tests, the error might be quite elusive. The only way to truly find the problem is to search through each test. As a final note about debugging, the theme of this chapter is that although the Bourne shell detects and reports syntax errors, it is not very helpful in indicating exactly where they occur. Scripters are hereby warned. But with the hints given above and with plenty of practice, a good shell scripter can catch them rather quickly.

A. Shell Built-in Command Reference


The following text is taken directly from the Bourne shell manual page (sh(1)). It originated from a Sun SPARCstation running the Solaris 2.4 operating system, an SVR4 compliant UNIX. According to the man page, the information presented was last modified on October 11, 1993. Regardless of this fact, these commands should be consistent with any SVR4 or POSIX compliant system. Note that some descriptions will point to a man page by listing the command name followed by a number enclosed in parentheses. The reader is urged to refer to the declared man page for further reference. . filename Read and execute commands from filename and return. The search path specified by PATH is used to find the directory containing filename. :

90 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

No effect; the command does nothing. A zero exit code is returned. break [ n ] Exit from the enclosing for or while loop, if any. If n is specified, break n levels. continue [ n ] Resume the next iteration of the enclosing for or while loop. If n is specified, resume at the n-th enclosing loop. cd [ argument ] Change the current directory to argument. The shell parameter HOME is the default argument. The shell parameter CDPATH defines the search path for the directory containing argument. Alternative directory names are separated by a colon (:). The default path is null (specifying the current directory). Note: The current directory is specified by a null path name, which can appear immediately after the equal sign or between the colon delimiters anywhere else in the path list. If argument begins with a / the search path is not used. Otherwise, each directory in the path is searched for argument. echo [ argument ... ] Echo arguments. See echo(1) for usage and description. eval [ argument ... ] The arguments are read as input to the shell and the resulting command(s) executed. exec [ argument ... ] The command specified by the arguments is executed in place of this shell without creating a new process. Input/output arguments may appear and, if no other arguments are given, cause the shell input/output to be modified. exit [ n ] Causes a shell to exit with the exit status specified by n. If n is omitted the exit status is that of the last command executed (an EOF will also cause the shell to exit.) export [ name ... ] The given names are marked for automatic export to the environment of subsequently executed commands. If no arguments are given, variable names that have been marked for export during the current shell's execution are listed. (Variable names exported from a parent shell are listed only if they have been exported again during the current shell's execution.) Function names are not exported. getopts Use in shell scripts to support command syntax stan- dards (see intro(1)); it parses positional parameters and checks for legal options. See getoptcvt(1) for usage and description. hash [ -r ] [ name ... ] For each name, the location in the search path of the command specified by name is determined and remembered by the shell. The -r option causes the shell to forget all remembered locations. If no arguments are given, information about remembered commands is presented. Hits is the number of times a command has been invoked by the shell process. Cost is a measure of the work required to locate a command in the search path. If a command is found in a "relative" directory in the search path, after changing to that directory, the stored location of that command is recalculated. Commands for which this will be done are indicated by an asterisk (*) adjacent to the hits information. Cost will be incremented when the recalculation is done. newgrp [ argument ] Equivalent to exec newgrp argument. See newgrp(1M) for usage and description. pwd Print the current working directory. See pwd(1) for usage and description.

91 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

read name ... One line is read from the standard input and, using the internal field separator, IFS (normally space or tab), to delimit word boundaries, the first word is assigned to the first name, the second word to the second name, etc., with leftover words assigned to the last name. Lines can be continued using \newline. Characters other than newline can be quoted by precedin g them with a backslash. These backslashes are removed before words are assigned to names, and no interpretation is done on the character that follows the backslash. The return code is 0, unless an EOF is encountered. readonly [ name ... ] The given names are marked readonly and the values of the these names may not be changed by subsequent assignment. If no arguments are given, a list of all readonly names is printed. return [ n ] Causes a function to exit with the return value specified by n. If n is omitted, the return status is that of the last command executed. set [ --aefhkntuvx [ argument ... ] ] -a Mark variables which are modified or created for export. -e Exit immediately if a command exits with a nonzero exit status. -f Disable file name generation. -h Locate and remember function commands as functions are defined (function commands are normally located when the function is executed). -k All keyword arguments are placed in the environment for a command, not just those that precede the command name. -n Read commands but do not execute them. -t Exit after reading and executing one command. -u Treat unset variables as an error when substituting. -v Print shell input lines as they are read. -x Print commands and their arguments as they are executed. -- Do not change any of the flags; useful in setting $1 to -. Using + rather than - causes these flags to be turned off. These flags can also be used upon invocation of the shell. The current set of flags may be found in $- . The remaining arguments are positional parameters and are assigned, in order, to $1, $2, .... If no arguments are given the values of all names are printed. shift [ n ] The positional parameters from $n+1 ... are renamed $1 ... . If n is not given, it is assumed to be 1. stop pid ... Halt execution of the process number pid. (see ps(1)). test Evaluate conditional expressions. See test(1) for usage and description. times Print the accumulated user and system times for processes run from the shell. trap [ argument ] [ n ] ... The command argument is to be read and executed when the shell receives numeric or symbolic signal(s) (n). (Note: argument is scanned once when the trap is set and once when the trap is taken.) Trap commands are executed in order of signal number or corresponding symbolic names. Any attempt to set a trap on a signal that was ignored on entry to the current shell is ineffective. An attempt to trap on signal 11 (memory fault) produces an error. If argument is absent all trap(s) n are reset to their original values. If argument is the null string this signal is ignored by the shell and by the commands it invokes. If n is 0 the command argument is executed on exit from the shell. The trap command with no arguments prints a list of commands associated with each signal number. type [ name ... ] For each name, indicate how it would be interpreted if used as a command name. ulimit [ -[HS][a | cdfnstv] ] OR ulimit [ -[HS][c | d | f | n | s | t | v] ] limit ulimit prints or sets hard or soft resource limits. These limits are described in getrlimit(2). If limit is not present, ulimit prints the specified limits. Any number of limits may be printed at one time. The -a
92 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

option prints all limits. If limit is present, ulimit sets the specified limit to limit. The string unlimited requests the largest valid limit. Limits may be set for only one resource at a time. Any user may set a soft limit to any value below the hard limit. Any user may lower a hard limit. Only a super-user may raise a hard limit; see su(1M). The -H option specifies a hard limit. The - S option specifies a soft limit. If neither option is speci- fied, ulimit will set both limits and print the soft limit. The following options specify the resource whose limits are to be printed or set. If no option is specified, the file size limit is printed or set. -c maximum core file size (in 512-byte blocks) -d maximum size of data segment or heap (in kbytes) -f maximum file size (in 512-byte blocks) -n maximum file descriptor plus 1 -s maximum size of stack segment (in kbytes) -t maximum CPU time (in seconds) -v maximum size of virtual memory (in kbytes). umask [ nnn ] The user file-creation mask is set to nnn (see umask(1)). If nnn is omitted, the current value of the mask is printed. unset [ name ... ] For each name, remove the corresponding variable or function value. The variables PATH, PS1, PS2, MAILCHECK, and IFS cannot be unset. wait [ n ] Wait for your background process whose process id is n and report its termination status. If n is omitted, all your shell's currently active background processes are waited for and the return code will be zero.

C. Index
From the list below, select the letter that is the first character of the topic you want to find:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
. ; ;; () {} [] < << <& <&> >> >& >&& && | || '' "" `` \

93 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

A
and, -a arguments functions as arguments to test conditions of for loops of functions arithmetic using bc using expr

B
background, & back quotes, `` see command substitution
bc

reading from files


break

sh man page description

C
case

double semi-colons, ;;, in case statement parentheses, (), in case statements regular expressions in case statements syntax
cd

sh man page description command line debugging command line options colon, : sh man page description command substitution, `` debugging scripts for loop argument effects of eval nesting commands comments, # inline line
continue

sh man page description curly braces, {} function delimiter dereferencing variables using for grouping v. variables

D
debugging scripts

94 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

command line debugging examples of when to use declaring the shell null commands interpreting errors examples malformed if malformed test missing arguments missing quotes how the shell reports syntax errors pausing a script's execution continuing a paused script example terminating a paused script using set -x and read tracing a script's execution enabling with a script option disabling tracing tracing example using in conjunction with pausing using set -x using set -xv using sh -x
do

in for loops in until loops in while loops


done

in for loops in until loops in while loops dot sh man page description double ampersand, && double quotes, "" escaping metacharacters patterns containing white space testing strings using with functions as arguments to test conditions using with escape using with single quotes double semi-colons, ;; double vertical bar, ||

E
echo

sh man page description


elif else

environment variables escape, \ escaping metacharacters

95 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

line continuation using with double quotes


esac eval

behavior definition sh man page description


exec

definition script I/O


exit

handling errors with sh man page description


export

sh man page description


expr

F
false fi

file descriptors redirection stderr stdin stdout filename expansion as for loop argument effects of eval special characters for loop argument list generating behavior definition intermediate variable usage positional parameters usage syntax functions as an argument to test conditions quoting declaration argument list body delimiter label rules definition invoking modularity including in multiple scripts nesting performance improving script performance vs sourcing
96 de 102 11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

vs subscripts properties return value checking with $? default returning from a function scope of nested functions of variables declared by functions

G
getopts

behavior definition OPTARG variable processing command line options sh man page description syntax usage

H
hash

sh man page description

I
if

nesting if blocks syntax usage I/O devices


/dev/console /dev/null /dev/tty

standard I/O

J
job control

K L
loops for loops loop control break continue while loops

97 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

until loops

M
metacharacters table of

N
newgrp sh man page description

O
operators arithmetic file negation of table of integral list of logical and, -a or, -o usage string table of options arguments to definition forms separated stacked processing
getopts

positional parameters shell options, set


OPTARG or, -o

P
parameters positional as function arguments as for loop arguments changing with set handling command line options with special parameter substitution parentheses, () role in function declaration usage in case statements period

98 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

See dot pipe, | filtering with


pwd

sh man page description

Q
quoting forms of testing strings with quotes

R
read

pausing script execution reading from files sh man page description


readonly

sh man page description redirection file descriptors stderr into stdout input file based, < interactive, << syntax loops output creating new files with, > appending to existing files, >> syntax script I/O regular expressions using in case statements utilities table of
return

checking return value with $? default value function control sh man page description syntax

S
scripts argument for body comparison to essay declaring shell to use terminating
set

options

99 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

positional parameters sh man page description tracing scripts semicolon


shift

in while loops sh man page description single quotes, '' escaping metacharacters patterns containing white space using with double quotes signals definition detection of handling with trap list of sending to a program standard I/O closing file descriptors redirection reading files with script I/O stderr into stdout using exec using loops stderr stdin stdout
stop

sh man page description subscripts executing with exec performance, vs functions performance, vs sourcing sourcing synchronization

T
test

combining multiple tests into one file tests forcing a test result colon, :
false true

functions as arguments integer tests sh man page description square brackets as test, [] syntax vs test command string tests while loops

100 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

until times

loops

sh man page description


trap

behavior definition forms of properties sh man page description signals syntax


true type

sh man page description

U
ulimit

sh man page description


umask

sh man page description


unset until

sh man page description loop behavior definition syntax test condition

V
variables environment variables exporting positional parameters properties data types permissions read-only read-write scope when declared within a function special parameters storing command output filename expansion quotes white space value assignment using parameter substitution for defaults using read dereferencing using braces

101 de 102

11/10/2010 16:13

Bourne_Shell_Programming-Robert_P_Sayle.html

file:///Z:/VMWare_Shared/Bourne_Shell_Programming-Robert_P_Say...

W
wait

sh man page description while loop behavior definition shifting syntax test condition

X Y Z

102 de 102

11/10/2010 16:13

Você também pode gostar