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:
- Interactive login shell
- Non-interactive login shell
- Interactive shell
- Non-interactive shell
ssh localhost
ssh localhost <command>
bash
bash -c <command>
/etc/profile, ~/.bash_profile
- (same)
~/.bashrc
- 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:
In
BASH_INTERACTIVE=true
BASH_LOGIN=false
~/.bash/init
.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:
There are two more things here to explain. First, I record successfully sourced profiles in
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"
$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
:
Environment variables usually only need to be set once at login when they are exported.
$BASH_INTERACTIVE || return 1
$BASH_LOGIN || return 1
if which -s tmux; then
echo "Open sessions:"
tmux list-sessions 2> /dev/null || true
fi
/usr/local/etc/bash/profiles/default-login
:
With functions and aliases it is different. Bash does not pass them to subshells, so they need to be sourced again and again.
$BASH_LOGIN || return 1
$BASH_SOURCED && return 1
export PATH="$PATH:~/bin"
/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 ...
Code is on http://forge.puppetlabs.com/anselm/bash
ReplyDeleteVery nice writeup. Thanks for taking the time to do this.
ReplyDelete