Classic Shell Scripting - Arnold Robbins [101]
# to the first instance of each file found on the search path,
# or "filename: not found" on standard error.
#
# The exit code is 0 if all files are found, and otherwise a
# nonzero value equal to the number of files not found (subject
# to the shell exit code limit of 125).
#
# Usage:
# pathfind [--all] [--?] [--help] [--version] envvar pattern(s)
#
# With the --all option, every directory in the path is
# searched, instead of stopping with the first one found.
In a networked environment, security has to be given serious consideration. One of the insidious ways that shell scripts can be attacked is by manipulating the input field separator, IFS, which influences how the shell subsequently interprets its input. To prevent this kind of attack, some shells simply reset IFS to a standard value before executing any script; others happily import an external setting of that variable. We prevent that by doing the job ourselves as the first action in our script:
IFS='
'
It is hard to see on a screen or a printed page what appears inside the quotes: it is the three-character string consisting of a newline, a space, and a tab. The default value of IFS is space, tab, newline, but if we write it that way, a whitespace-trimming editor might eliminate trailing spaces, reducing the string's value to just a newline. It would be better to be able to write it with explicit escape characters, such as IFS="\040\t\n", but regrettably, the Bourne shell does not support those escape sequences.
There is one subtle point that we need to be aware of when redefining IFS. When "$*" is expanded to recover the command line, the first character of the value of IFS is used as the field separator. We don't use $* in this script, so our rearrangement of characters in IFS does not matter.
Another common way to break security is to trick software into executing unintended commands. To discourage this, we want programs that we invoke to be trusted versions, rather than imposters that might be lurking in a user-provided search path. We therefore reset PATH to a minimal value, saving the original value for later use:
OLDPATH="$PATH"
PATH=/bin:/usr/bin
export PATH
The export statement is crucial: it ensures that our secure search path is inherited by all subprocesses.
The program code continues with five short functions, ordered alphabetically for reader convenience.
The first function, error( ), prints its arguments on standard error, and then calls a function, to be described shortly, that does not return:
error( )
{
echo "$@" 1>&2
usage_and_exit 1
}
The second function, usage( ), writes a brief message showing the expected way to use the program, and returns to its caller. Notice that the function needs the program name, but doesn't hardcode it: it gets it from the variable PROGRAM, which will shortly be set to the name by which the program was invoked. This permits an installer to rename the program without modifying the program code, in the event that there is a collision with an already-installed program with an identical name but different purpose. The function itself is simple:
usage( )
{
echo "Usage: $PROGRAM [--all] [--?] [--help] [--version] envvar pattern(s)"
}
The third function, usage_and_exit( ), produces the usage message, and then exits with a status code given by its single argument:
usage_and_exit( )
{
usage
exit $1
}
The fourth function, version( ), displays the program version number on standard output, and returns to its caller. Like usage( ), it uses PROGRAM to obtain the program name:
version( )
{
echo "$PROGRAM version $VERSION"
}
The fifth and last function, warning( ), prints its arguments on standard error, increments the variable EXITCODE by one to track the number of warnings issued, and returns to its caller:
warning( )
{
echo "$@" 1>&2
EXITCODE=`expr $EXITCODE + 1`
}
We discussed expr in more detail in Section 7.6.3. Its usage here is a common shell idiom for incrementing a variable. Newer shells permit the simpler form EXITCODE=$((EXITCODE