October 28, 2010

About those bash profiles

Which profile?

It seems like a common fact to me that most people (including myself) get stuck when having to choose the right profile file in bash to add customizations. While there is actually a comprehensive manual that explains everything, I never found it clear enough, because I kept re-opening it reading the same sections over and over again. So once upon a time I took the courage trying to solve this quest. I started to hack and what came out is indeed not the simplest way of initializing a shell ...

My wish was actually simple. For each setting (like an environment variable or function definition) I wanted to be able to specify exactly for what type or types of shells it should be set. And additionally I wanted to set those settings either system-wide or per user.

Shell types

The type of a bash shell is determined by 3 boolean properties. A bash can be interactive, it can be a login shell and it can be a subshell of an existing one. The last property is not particularly interesting for now, I will use it later on when deciding whether a setting should be initialized only once or repeatedly for every subshell. This leaves us with 2 properties, that, if combined, give 4 different shell types:

  1. Interactive login shell
  2. Non-interactive login shell
  3. Interactive shell
  4. Non-interactive shell
To make it more clear, an example for each type:
  1. ssh localhost
  2. ssh localhost <command>
  3. bash
  4. bash -c <command>
A interactive login shell is what you get when you login with SSH on a remote host. From there you will mostly start subshells that are either interactive or not. If you give SSH a shell command as argument it will login, execute the command, and exit again, without giving you an interactive prompt. So that is a non-interactive login shell. Each type sources certain profile files upon initialization:
  1. /etc/profile, ~/.bash_profile
  2. (same)
  3. ~/.bashrc
  4. File defined in $BASH_ENV

Initializing the shell

I want to have a global system folder /usr/local/etc/bash/profiles/ and a per-user folder ~/.bash_profiles/ where I can put profile files and bash will source them automatically. Per user settings should overwrite global ones and within each profile I can choose what shell type it will apply for. To do this I first configure .bashrc and .bash_profile. I simply set the variables $BASH_LOGIN and $BASH_INTERACTIVE to either true or false depending on the shell type.

For .bashrc this is easy since it is only sourced by interactive shells that are not login shells:

BASH_INTERACTIVE=true
BASH_LOGIN=false

~/.bash/init
In .bash_profile I have to distinguish between interactive and non-interactive login shells. This can be done by looking at $-.
BASH_LOGIN=true

if [[ $- = *i* ]]; then
  BASH_INTERACTIVE=true
else
  BASH_INTERACTIVE=false
fi

. ~/.bash/init

From both files I call a global initialization file that will look for profile files and source them. Inside each profile I can then choose if I want to apply the settings depending on $BASH_LOGIN and $BASH_INTERACTIVE. The common init looks like this:

for profile in ~/.bash/profiles/* ~/.bash_profiles/*; do
  if [ -f $profile ]; then
    if [[ "$BASH_PROFILES" = "*$profile*" ]]; then
      BASH_SOURCED=true
    else
      BASH_SOURCED=false
    fi
    . $profile && \
      export BASH_PROFILES="${BASH_PROFILES}${profile} "
  fi
done
unset profile BASH_SOURCED

BASH_ENV="~/.bash/bash_env"
There are two more things here to explain. First, I record successfully sourced profiles in $BASH_PROFILES and set $BASH_SOURCED to true if the profile has already been sourced. So optionally I can cause profiles to not get sourced again in a sub-shell. This is handy for exported variables that you do not want to overwrite if the user changed the value and exported it from an initial shell. Second, I set $BASH_ENV globally in init, so it is set for every shell. This is for non-interactive non-login shells. Unfortunately bash does not source any files for this type directly, but instead the file specified by this variable. In my case bash_env looks simply like this:
BASH_LOGIN=false
BASH_INTERACTIVE=false

. ~/.bash/init

Logging out

Here the process is much more simple. Bash does only source one file .bash_logout and only if a login shell exits. Also, it does not make sense to setup anything on logout and the settings will not be passed on. So I simply check if it is a interactive shell and source profiles from a separate logout directory:

if [[ $- = *i* ]]; then
  BASH_INTERACTIVE=true
else
  BASH_INTERACTIVE=false
fi

for profile in ~/.bash/profiles/logout/* \
~/.bash_profiles/logout/*; do
  if [ -f $profile ]; then
    . $profile
  fi
done
unset profile

Skeleton

Finally I want to put these files into a global folder and only link from my user home to them. On my FreeBSD boxes I put them in /usr/local/etc/bash/ and create a set of skeleton symbolic links in /usr/local/etc/bash/skel/ that every user can copy to his home:

/usr/local/etc/bash/:

bash_env
bash_logout
bash_profile
bashrc
init
profiles
skel

/usr/local/etc/bash/skel/:

.bash -> /usr/local/etc/bash
.bash_logout -> .bash/bash_logout
.bash_profile -> .bash/bash_profile
.bashrc -> .bash/bashrc

Examples

tmux is a text window manager similar to screen. I like to see any open sessions upon login, but do not want get the list on every sub shell I start or when the shell is not interactive. /usr/local/etc/bash/profiles/tmux-list:

$BASH_INTERACTIVE || return 1
$BASH_LOGIN || return 1

if which -s tmux; then
  echo "Open sessions:"
  tmux list-sessions 2> /dev/null || true
fi
Environment variables usually only need to be set once at login when they are exported. /usr/local/etc/bash/profiles/default-login:
$BASH_LOGIN || return 1
$BASH_SOURCED && return 1

export PATH="$PATH:~/bin"
With functions and aliases it is different. Bash does not pass them to subshells, so they need to be sourced again and again. /usr/local/etc/bash/profiles/alias-functions:
$BASH_INTERACTIVE || return 1

function savealias() {
  echo "!!! This command is aliased for safety reasons." >&2
  echo "!!! If you are serious use `type -P $1`." >&2
  return 1
}

function ask() {
  echo -n "Enter 'y' to proceed: "
  read answer
  [ "$answer" = 'y' ] && return 0
  return 1
}

alias shutdown="savealias shutdown"
alias halt="savealias halt"
alias reboot="ask && /sbin/shutdown -r now"
alias init="savealias init"

Whining comment

Maybe I should write a patch for bash entitled "Proper profile initialization", I just can not decide if I should call it a feature or bug report ...