Skip to main content

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 \
    DEMO_ENV_SER="$(declare -p DEMO_ENV)" \
    bash --init-file "${BASH_SOURCE[0]}"

  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: