So, you have a wonderful script and you want to be sure that it always gets invoked with envdir. A feature of envdir is that it runs a defined program (see man envdir). This means it's not possible to use envdir inside a script, for use by the script itself. Or is it? This write-up will walk you though some ways of getting around this problem.
So, how would you make sure that your script gets called by envdir? Well, you could ask the invoker to always call it with:
/usr/bin/envdir $ENV_DIR /bin/sh yourShellScript.sh
You are however not guaranteed that the invoker will always do this, especially if the invoker is a human.
You could also write another script that will use envdir to call your script, like this:
#!/bin/sh
/usr/bin/envdir $ENV_DIR /bin/sh yourShellScript.sh
If you can live with having a wrapper script, as above, you need not read any further. The problem with this solution is however that you end up with 2 scripts.
To get around this, you could wrap your script with something, keeping it all in the same script:
#!/bin/sh
if [ -z $ENV_DIR ]; then ENV_DIR=$PWD/env; fi
if [ "$BS_ENVDIR_PPID" != "$PPID" ]
then
echo "Bootstrapping with: /usr/bin/envdir $ENV_DIR /bin/sh $0 $@"
if [ ! -d $ENV_DIR ]; then mkdir -p $ENV_DIR; fi
echo $$ >$ENV_DIR/BS_ENVDIR_PPID
/usr/bin/envdir $ENV_DIR /bin/sh $0 $@
else
# This is where the real script needs to be.
echo "Running the envdir-bootstapped script because \
the BS_ENVDIR_PPID, $BS_ENVDIR_PPID, is the same as my PPID, $PPID."
fi
The above script starts off setting $ENV_DIR to $PWD/env, if it was not set by the invoker. It then checks $BS_ENVDIR_PPID (the bootstrapped envdir parent pid). If $BS_ENVDIR_PPID is not the same as $PPID, then it bootstraps itself with /usr/bin/envdir $ENV_DIR /bin/sh $0 $@, on which it will fail on the '$BS_ENVDIR_PPID != $PPID' check and execute the part of the script that was intended to be invoked after calling envdir.
There are 4 minor down-sides of this wrapper:
- The real script gets wrapped in an if-then-else statement.
- The script forks itself, which means you end up with 2 processes.
- If $BS_ENVDIR_PPID is set, before invoking the script, there is a small chance (&le 1:max_pid) that the '$BS_ENVDIR_PPID != $PPID' check will fail, causing the envdir bootstrapping to be skipped. It should however be up to the invoker to make sure that $BS_ENVDIR_PPID is not set in the environment, prior to calling the script.
- An attacker could pretty easily guess $BS_ENVDIR_PPID and somehow have it set, prior to the first invocation of the script, but then again, it's probably easier to fake some contents of the $ENV_DIR directory or even the ENV_DIR variable itself.
I personally would not like to impose the restrictions mentioned in points 1, 3 and 4, on the invoker, hence the next trick.
#!/bin/bash
if [ -z $ENV_DIR ]; then ENV_DIR=$PWD/env; fi
NUM_LINES=`wc -l $0 |cut -d' ' -f1`
tail -$(($NUM_LINES-$LINENO)) $0 | /usr/bin/envdir $ENV_DIR sh -s $@; exit $?;
# This is where the real script needs to be.
echo "Running the envdir-bootstapped script."
Here, the trick is to only fork-off the part after the line that does the envdir bootstrapping. We start off by setting $ENV_DIR to $PWD/env, if it was not set by the invoker. We then set $NUM_LINES to the number of lines in the script. To be able to know what part of the script needs to be forked, we need to know the position of the forking start-position in the file. This can be calculated if we use $LINENO, a feature of at least 'bash'. The standard shell, 'sh', does not have this functionality.
There are 2 minor down-sides of this wrapper:
- We need a shell, like bash, that supports setting the $LINENO (or equivalent) variable.
- The script forks itself, which means you end up with 2 processes.
In order to remove the dependency on bash, and compress things a bit more, one could simply add the following 3 hard-coded lines of code at the top of a script:
#!/bin/sh
if [ -z $ENV_DIR ]; then ENV_DIR=$PWD/env; fi
/usr/bin/tail -$((`/usr/bin/wc -l $0 |/usr/bin/cut -d' ' -f1`-3)) $0 | /usr/bin/envdir $ENV_DIR /bin/sh -s $@; exit $?;
This is lean, mean and the '3' in the third line is hard-coded, so you won't need a special shell that supplies you with the number of the currently executed line. The only restriction is that these 3 line are the first 3 lines of your script.
At this point, it's worth mentioning that we could also fork other scripting languages like this. All we need to do is to tell envdir what program it should run. In the following example we we'll tell envdir to call a Node.js script:
#!/bin/sh
if [ -z $ENV_DIR ]; then ENV_DIR=$PWD/env; fi
/usr/bin/tail -$((`/usr/bin/wc -l $0 |/usr/bin/cut -d' ' -f1`-3)) $0 | /usr/bin/envdir $ENV_DIR /usr/bin/nodejs; exit $?;
// The node.js script follows:
console.log('Node.js said: Hello World');
Here's another example that runs GNU/make:
#!/bin/sh
if [ -z $ENV_DIR ]; then ENV_DIR=$PWD/env; fi
/usr/bin/tail -$((`/usr/bin/wc -l $0 |/usr/bin/cut -d' ' -f1`-3)) $0 | /usr/bin/envdir $ENV_DIR make -sf - $@; exit $?;
# The Makefile follows:
all:
@echo "make said: Hello World"
Finally, this is not rocket science and I'm sure many other people have implemented similar or even the same tricks before.