Classic Shell Scripting - Arnold Robbins [102]
Even though this program is short enough that we don't really need functions, except for avoiding code duplication, it is good programming practice to hide irrelevant details: tell what we're doing, but not how we do it.
We have now reached the point where the first statement is executed at runtime. We initialize five variables to record an option choice, the user-provided environment variable name, the exit code, the program name, and the program version number:
all=no
envvar=
EXITCODE=0
PROGRAM=`basename $0`
VERSION=1.0
In our program, we follow the convention that lowercase variables are local to functions or to the main code body, whereas uppercase ones are shared globally by the entire program. We use a string value, instead of a number, for the variable all because it makes the program clearer, with negligible runtime cost.
* * *
Tip
The basename command is the traditional tool for extracting the filename part of a complete pathname: it strips all leading characters of its first argument up to and including the final slash, and reports the remainder on standard output:
$ basename resolv.conf
Report the bare filenameresolv.conf
$ basename /etc/resolv.conf
Report the bare filenameresolv.conf
Descendants of the Bourne shell offer the pattern-matching operators shown in Table 6-2 in Chapter 6 for this purpose, but basename is an ordinary command, and thus works with all shells.
With a second argument representing a filename suffix, basename strips any matching suffix from its result:
$ basename /etc/resolv.conf .conf
Report the suffixless filename
resolv
$ basename /etc/resolv.conf .pid
Report the bare filename
resolv.conf
Although basename's first argument is normally a pathname, basename simply treats it as a text string, and does not require, or check, that it corresponds to a real file.
If the argument is omitted, or is an empty string, basename's behavior is implementation-defined.
* * *
The big block of code that follows is typical of command-line argument parsing in all Unix programs: while we have an argument (determined by the argument count, $#, being greater than zero), process that argument in a case statement that selects a code block according to the string value of the argument:
while test $# -gt 0
do
case $1 in
The case selectors bear some explanation. GNU programming style encourages long, descriptive option names, rather than the old cryptic, single-character options historically used in Unix. Such brevity is acceptable when the option count is small and the program is used frequently. Otherwise, descriptive names are better, provided that we permit the user to specify just enough of them to guarantee uniqueness. However, such abbreviations should be avoided when these options are supplied from other programs, since that makes the code clearer to a human, and guards against surprises when new options are added to later versions of the program.
There isn't any simple way in the shell language to specify that a long name can be matched by a leading unambiguous prefix of that name, so we just have to supply all of the alternatives.
Long option names retrofitted into older programs are sometimes distinguished from the original options by a leading double hyphen. For new code, we therefore allow either one or two hyphens, which is easily accommodated by duplicating the abbreviations in the case selectors and adding an extra hyphen.
We could write the case selectors with wildcard matching: --a* | -a* ). However, we view that as unacceptably sloppy practice, since it allows matches against completely different names than those documented.
For the —all option, we simply record the fact that the option was found by resetting the variable all to yes:
--all | --al | --a | -all | -al | -a )
all=yes
;;
The doubled semicolons are mandatory after each case block, except for the last. We could have written this block more compactly:
--all | --al | --a | -all | -al |