A wrapper script is one of the most common uses for shell script. Rather than attempting to reimplement the functionality of an existing tool, a wrapper's job is to pass along its inputs to some other tool, with slight modifications. This saves a tremendous amount of labor.

The most basic form of a wrapper script is this:

   1 #!/bin/sh
   2 exec /some/weird/place/toolname ${1+"$@"}

Generally, when writing a wrapper, we use the most portable syntax possible. There's normally no need for fancy, shell-specific or platform-specific code if all we're doing is passing along inputs to some other program. In fact, the code above could probably have been replaced with a symbolic link. A slightly more realistic wrapper script might look something like this:

   1 #!/bin/sh
   2 CDR_SECURITY=8:dvd,clone,....
   3 export CDR_SECURITY
   4 exec cdrecord-prodvd ${1+"$@"}

This wrapper sets an environment variable, and then invokes another program with the same arguments and input that the wrapper script received.

The #!/bin/sh shebang is used because we don't need anything that's not present in the vanilla Bourne shell. We're sticking to the most portable syntax, so this wrapper could run on any Unix system we're likely to find. Thus, the export is on a separate line, after the variable it's exporting has been set to its value.

The exec causes the tool we're wrapping to have the same PID that we have. This is important if the process is being managed by something that wants to maintain a parent/child relationship with it for purposes of sending signals, or simply for recording the process's PID. Also, there's no need to leave an instance of the shell lying around in memory; the exec saves one process fork, and is therefore quite efficient.

Recall that only non-scalar parameters (parameters that can expand to multiple words when double-quoted - *, @, array[@] and array[*]) expand to no words when unset. Therefore "${1+"$@"}", "$1", "${a[0]}", and so fourth, will all expand to at least one word when double quoted, even if unset.

The {1+"$@"} hack is used instead of "$@" because some old versions of the Bourne shell had a bug, in which an empty argument list would cause "$@" to expand to a single string of length 0 (that is, '') instead of an empty argument list. The ${1+...} syntax gets around that by checking first whether we have a non-empty argument list. If we do, then we use "$@" to pass it along; otherwise, we don't pass anything. You'll see this construct many, many times. (For more examples of fancy parameter expansions, see Bash FAQ #73.)

Another, more complicated, case of a wrapper script is a daemontools run script. Services that are managed by daemontools rely on a program (written by the system administrator, usually in Bourne shell) to set up the environment and execute the process that's being managed. Here's a (somewhat shortened) example:

   1 #!/bin/sh
   2 exec /usr/local/bin/softlimit -m 3000000 \
   3     /usr/local/bin/tcpserver -v -x /etc/tcp.smtp.cdb -u 10092 -g 10098 0 smtp \
   4     /usr/local/bin/rblsmtpd -r zen.spamhaus.org \
   5     /var/qmail/bin/qmail-smtpd 2>&1

This example is interesting because it's really an onion-like set of layers. (All of DJB's software is designed that way.) Our run script is a Bourne shell script that execs a program called softlimit. softlimit's job is simply to set up process resource limits (see setrlimit(2) on GNU/Linux). We could use a shell's ulimit or limit command to do the same thing, but remember that we're writing in Bourne shell, which doesn't have that! So, DJB wrote softlimit as a wrapper program (in C) which sets the limits and then execs the next process in the chain.

(If we were writing our run script in bash instead of Bourne shell, then calling ulimit would be efficient and desirable.)

The 2>&1 at the end of the exec softlimit command binds standard error and standard output together, because another process (not shown) is logging all of this stuff.

The next process in the chain is tcpserver from DJB's ucspi-tcp suite. This inherits the limits already set by the previous elements of the chain. Then, it reads the /etc/tcp.smtp.cdb file, and starts listening on the smtp port of network interface 0 (all interfaces). When it receives a connection, it forks and executes the rblsmtpd program as UID 10092 and GID 10098. (It also sets several different environment variables, but that's a bit beyond the scope of this page.)

The rblsmtpd program inspects its environment variables (inherited from earlier in the process chain), and based on those, it'll either do DNS lookups of the connection's source IP address, or skip those lookups. If the lookups are performed and find a "spam sender" result, an error is written, and rblsmtpd exits. Otherwise, rblsmtpd execs the next process in the chain.

Finally, if we got this far, qmail-smtpd actually receives an email from the sender and puts it in the queue for processing.

CategoryShell CategoryUnix

WrapperScript (last edited 2012-03-06 17:42:56 by ormaaj)