Running Base in a New Shell
Base configures Bash shell environments relative to a directory. It provides an easy and consistent way to load the configuration for diverse projects.
Base started out as a simple ${HOME}/bin
script in 2007
and was gradually improved on over the years. In 2011, I first released
it as open source in my company GitHub account. When I shut down the
company and removed the GitHub account, the project went offline. I have
since done a complete rewrite and am currently preparing to push the
second major release to GitHub.
The pending release has many improvements over previous versions. This blog entry is about one of the new features: configuring the environment in a new Bash shell.
Overview
In previous versions of Base, an environment is configured by “sourcing” the program as follows:
$ . base
The .
command (which can also be written
source
) executes the base
script in the
current shell, and the base configuration is added to the
current environment. The base_deactivate
command can later
be used to remove the base configuration, restoring any environment
variables that were modified by the base configuration.
Modifying the current environment works well, but it takes extra
effort to support deactivation. It would be nice to configure the
environment in a new shell, where one could simply exit
the
shell to return to the previous environment. After quite a bit of
prototyping, I have a solution that I am somewhat satisfied with, where
the current environment is recreated in a new shell.
The second major release supports both methods, as I will not be confident that the new method is sufficient without using it for some time. Unfortunately, support for deactivation is still necessary.
Notice
The following script has bugs. The fixes are discussed in other blog entries, listed below. The final version of the script can be found in Abort Transformation!
Demo Script
The following is a minimal prototype of a Bash script that recreates
the current environment in a new shell. This script, sans commentary, is
available on GitHub: demo.sh
The script needs access to the full environment of your current
shell, so it must be sourced instead of executed in a new process. To
try it out, be sure to set the permissions of the script to make it
executable and run . demo.sh
(or
source demo.sh
) to recreate your current environment in a
new shell.
#!/usr/bin/env bash
The _demo_load_env
function queries the current Bash
shell environment and outputs configuration commands that are to be
executed in the new Bash shell environment.
Environment variables are queried using the declare -p
command. Some environment variable values may contain newlines, so the
environment variable names are queried first, and then each environment
variable is queried separately. Newlines and tabs are escaped in the
output. Note that some environment variables that should not be copied
are filtered from the output.
This function prints the commands to STDOUT
so that they
can be easily loaded into an array using readarray
below.
_demo_load_env () {
local var
while IFS=$'\n' read -r var ; do
case "${var}" in
BASH_* | FUNCNAME | GROUPS | cmd | val )
;;
DEMO_ENV | var )
;;
* )
declare -p "${var}" | sed -e 's/\t/\\t/' -e '$! s/$/\\n/' | tr -d '\n'
echo
;;
esac
done < <(declare -p | grep '^declare' | sed 's/^declare -[^ ]* \([^=]\+\).*$/\1/')
alias -p
}
When sourced, the current environment configuration is loaded into an
array. A new Bash shell process is executed using the script for
initialization. The current environment configuration is serialized and
passed to the new process using the DEMO_ENV_SER
environment variable.
When the new shell process exits (presumably when the user types
exit
), the remaining environment variable is removed and
the source
command is exited. (Note that it would be
preferable to exit with the exit status of the new shell process, but I
do not know of a good way to do so in this case.)
if [ -z "${DEMO_ENV_SER}" ] ; then
if [ "${BASH_SOURCE[0]}" == "${0}" ] ; then
echo "usage: . ${0}" >&2
exit 2
fi
declare -a DEMO_ENV
readarray -t DEMO_ENV < <(_demo_load_env)
unset -f _demo_load_env
/usr/bin/env \
"$(declare -p DEMO_ENV)" \
DEMO_ENV_SER=--init-file "${BASH_SOURCE[0]}"
bash
unset DEMO_ENV
return 0
fi
The rest of the script is executed during initialization of the new
Bash shell process. The _demo_load_env
function is no
longer needed, so it is unset.
unset -f _demo_load_env
The _demo_restore_env
function loops through
configuration commands and determines which ones should be evaluated. It
filters out environment variable declarations for which the environment
variable already exists and is read-only.
Like _demo_load_env
, this function prints to
STDOUT
.
_demo_restore_env () {
local defcmd envcmd var
for rstcmd in "${DEMO_ENV[@]}" ; do
if [[ "${rstcmd}" =~ ^declare ]] ; then
defcmd="${rstcmd#declare -* }"
var="${defcmd%%=*}"
envcmd="$(declare -p "${var}" 2>/dev/null)"
if [[ -z "${envcmd}" || "${envcmd}" =~ ^declare\ -[^r\ ]*\ ]] ; then
echo "${rstcmd}"
fi
else
echo "${rstcmd}"
fi
done
}
The new Bash shell is initialized by evaluating the serialized
configuration commands, restoring the array, and then evaluating the
commands selected by the _demo_restore_env
function. Note
that this evaluation cannot be done within a function, where the
declarations would create variables local to the function. Finally, the
_demo_restore_env
function and implementation environment
variables are unset.
eval "${DEMO_ENV_SER}"
while IFS=$'\n' read -r rstcmd ; do
eval "${rstcmd}"
done < <(_demo_restore_env)
unset -f _demo_restore_env
unset DEMO_ENV DEMO_ENV_SER
The following is a simple test of this demonstration script:
$ echo "Current shell ID: $$"
Current shell ID: 45103
$ SECRET=11
$ bash
$ echo "New shell ID: $$"
New shell ID: 106847
$ echo "New shell does not have SECRET: ($SECRET)"
New shell does not have SECRET: ()
$ exit
exit
$ echo "Back to current shell: $$"
Back to current shell: 45103
$ . demo.sh
$ echo "New shell ID: $$"
New shell ID: 107155
$ echo "New shell has SECRET: ($SECRET)"
New shell has SECRET: (11)
Updates
This demonstration script has issues! The issues and fixes are discussed in the following blog entries: