the djb way

daemontools


parameterize with envdir

The envdir utility is used to setup environmental variables for a service, and is commonly found in "run" scripts along the djb way.

It's usage is simple:

envdir dir program ...

That is, envdir reads the directory specified by the dir argument, loads the environmental variables configured there, and runs program. The program will then have access to the environmental variables.

Environmental variables are configured like this: each filename found in dir is used as a variable name, the contents of that file are used as the value for that variable.

This is a common idiom on the djb way, where the filesystem itself as used as a database. A record key is given by the filename, the record data by the file contents. (Notice that one of the many consequences of this paradigm is the automatic elimination of duplicate keys.)

The dir argument to envdir is conventionally named ./env. That is, a directory named env is created within the run script directory, for use only by envdir. For example:

# cd /var/svc.d/myservice
# mkdir ./env
# echo 'hello' > ./env/HELLO
# echo 'see ya' > ./env/GOODBYE

Now when you run envdir on ./env, this will setup two environmental variables with values, as though you had defined and exported:

HELLO=hello
GOODBYE=see ya

Let's check it:

# cd /var/svc.d/myservice
# envdir ./env /usr/bin/printenv
...
HELLO=hello
GOODBYE=see ya

Here we run the system's printenv(1) as the "child" of envdir, as a way to view all the variables seen in the new environment, including the two new variables we just defined.

To check only the variables set up with envdir, use the system utility env(1) to clear out the current environment first:

# env -i /command/envdir ./env /usr/bin/printenv
HELLO=hello
GOODBYE=see ya

(Notice how full paths are now necessary to the executables here, since env -i has wiped out the $PATH variable too.)

A simple shell script is convenient for listing all the variables setup in an envdir; here is ours called envdir-ls:

# envdir-ls ./env
HELLO=hello
GOODBYE=see ya

This is a very handy way to check the contents of an envdir, and you get to see exactly what your service will be "thinking" of these variables.

Now on to explore envdir in a little more depth. Let's look at this first:

# envdir ./env /bin/sh -c 'echo "${GOODBYE}"'
see ya

Here we run sh(1) as the "child" of envdir, and use the built-in echo command to show the variable GOODBYE is set and available within its environment.

But here's something that won't work:

# envdir ./env /bin/echo "${GOODBYE}"
<nothing>

This doesn't work because the current shell you are working in tries to expand ${GOODBYE} before exec-ing the command-line. ${GOODBYE} is unset in the current environment; it is only available for the environment of the child process run by envdir.

There are a few special features of envdir:

The last point describes how to setup multi-line values for variables:

# printf "see ya\0later!" > ./env/GOODBYE
# envdir ./env /bin/sh -c 'echo "${GOODBYE}"'
see ya
later!

Here we use the printf(1) utility to embed a null in the value for the GOODBYE variable; envdir then converts this to a newline. Notice that we used the shell quoting rules to inhibit field splitting, preserving the newline in the output above. But if we removed the double quotes wrapping ${GOODBYE}, we would get this instead:

# envdir ./env /bin/sh -c 'echo ${GOODBYE}'
see ya later!

The newline disappears, converted to a single space by the field splitting action performed by the shell during variable expansion. Of course, sh masters will know that the IFS variable may also be used to suppress field splitting:

# envdir ./env /bin/sh -c 'IFS=""; echo ${GOODBYE}'
see ya
later!

The same, only using envdir to setup IFS:

# echo "" > ./env/IFS
# envdir ./env /bin/sh -c 'echo ${GOODBYE}'
see ya
later!

Here IFS is set to the empty string, so no field splitting is performed. This might be a good time to mention a subtle but important distinction between variables being empty and unset:

To create an empty variable for envdir, just echo an empty string into the file for the variable name:

# echo "" > ./env/WHATEVER
# envdir-ls ./env
WHATEVER=

To unset a variable with envdir, create a zero-length file for the variable name with touch:

# envdir ./env /usr/bin/printenv |grep "^PATH"
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin
# touch ./env/PATH
# envdir ./env /usr/bin/printenv |grep "^PATH"
<nothing!>

In the command lines above, we first showed how the PATH variable is set from the current environment. Then, after we touch-ed a file named PATH in the envdir, we can see that PATH is now unset. It is not just that PATH is empty, the variable itself no longer exists.

Yes, all this has strayed into some of the more arcane areas of the shell: using envdir for multi-line values and unset variables won't often be encountered in practice. The use of envdir is basically simple and easy. But if you do find something isn't working quite as expected, take another glance through these details for a possible explanation of the behavior you are observing.

At the risk of redundancy, we would like to note one of the potential "gotchas" in using envdir with run scripts. In many of our TCP/IP service scripts, we define a CONLIMIT variable to use with the -c option to tcpserver:


#!/bin/sh
#
CONLIMIT=11

exec \
  tcpserver -c ${CONLIMIT} \
  0 999 \
    myserver


We use this convention as an expedient and visible way to parameterize a common argument. Of course, we might have decided to stick CONLIMIT in an envdir instead, changing our "run" scripts to read something like:


#!/bin/sh
#
# THIS FAILS!

exec \
  envdir ./env \
    tcpserver -c ${CONLIMIT} \
    0 999 \
      myserver


Here we may think that $CONLIMIT will now be supplied by envdir. Unfortunately, the script fails for the same reason described earlier. That is, $CONLIMIT is not defined in the current environment in which this script runs.

To make this script work with the use of envdir, we need the extra indirection of running a sub-shell under envdir:


#!/bin/sh
#
# OK, envdir with subshell:

exec \
  envdir ./env /bin/sh -c '\
    exec tcpserver -c ${CONLIMIT} \
    0 999 \
      myserver '


Now tcpserver runs in the process environment setup by envdir, and $CONLIMIT is now defined. Please note: whenever using this idiom, the subshell also needs to use exec to replace itself with the service you are running.

In general, this use of envdir just to parameterize command-line arguments within a run script is not found very often. (One example exception is Bernstein's run script for the dnscache service.)

Instead, envdir is most often used to define environmental variables expected by the service itself. For example, the ucspi-ssl services may require several environmental variables to define SSL certificate and key locations. The "blabbyd" tutorial we began with also demonstrated the use of envdir, to parameterize certain silly variables used by the service.

envdir is quick and easy, and allows one to easily reconfigure services without editing the "run" script itself. It is a defining feature of the djb way, and we will see several more instances of envdir is the pages ahead.

The only remaining problem is this: how do you say "envdir" out loud? (Without sounding like Homer Simpson, that is...)


Copyright © 2004, Wayne Marshall.
All rights reserved.

Last edit 2004.10.04, wcm.