Configuring your login sessions with dot files

Unless otherwise specified, this documentation assumes you're using bash as your login shell, and are using Linux.

The way your system behaves with respect to the reading of "dot files" at login time, setting up aliases, setting up environment variables, and so on is all highly dependent on how you actually log in.

Console logins

Let's start with the simplest configuration: a local login on the Linux text console, without any graphical environment at all, without systemd in the picture. In this case, when you boot the computer, you eventually see a "login:" prompt. This prompt is produced by a program called getty(8) which runs on the tty (terminal) device. When you type a username, getty reads it and passes it to the program called login(1). login reads the password database and decides whether it needs to ask you for a password. Once you've provided your password, login "execs" your login shell, bash, with a hyphen in front of the process name ("-/bin/bash" for example). This leading hyphen is a special ancient hack which means "this is a login shell, not a regular shell".

Now, since bash is being invoked as a login shell, it reads /etc/profile first. On Linux systems, this will typically also source some or all files in /etc/profile.d (as suggested by the Linux Standard Base -- generally /etc/profile should include code for this). Then, bash looks in your home directory for .bash_profile, and if it finds it, it reads that. If it doesn't find .bash_profile, it looks for .bash_login, and if it doesn't find that, it looks for .profile (the standard Bourne/POSIX/Korn shell configuration file). Otherwise, it stops looking for dot files, and gives you a prompt.

Many Linux systems also have another layer called PAM which is relevant here. Before "execing" bash, login will read the /etc/pam.d/login file (or its equivalent on your system), which may tell it to read various other files such as /etc/environment. Other systems such as OpenBSD have an /etc/login.conf file which controls resource limits for various classes of user accounts. So you may have some extra enviroment variables, process limits, and so on, before your shell reads /etc/profile.

You may have noted that .bashrc is not being read in this situation. You should therefore always have source ~/.bashrc at the end of your .bash_profile in order to force it to be read by a login shell. If you use .profile instead of .bash_profile, you additionally need to test if the shell is bash first:

   1 # .profile
   2 if [ -n "$BASH" ] && [ -r ~/.bashrc ]; then
   3     . ~/.bashrc
   4 fi

Why is .bashrc a separate file from .bash_profile, then? There are a couple reasons. The first is performance -- when machines were extremely slow compared to today's workstations, processing the commands in .profile or .bash_profile could take quite a long time, especially on machines where a lot of the work had to be done by external commands (before Korn/Bash shells). So the difficult initial set-up commands, which create environment variables that can be passed down to child processes, are put in .bash_profile. The transient settings and aliases/functions which are not inherited are put in .bashrc so that they can be re-read by every new interactive shell.

The second reason why .bashrc is separate is due to work habits. If you work in an office setting with a terminal on your desk, you probably login one time at the start of each day, and logout at the end of the day. You may put various special commands in your .bash_profile that you want to run at the start of each day, when you login -- checking for announcements from management, etc. You wouldn't want those to be done every time you launch a new shell. So, having this separation gives you some flexibility.

Let's take a moment to review. A system administrator has set up a Debian system (which is Linux-based and uses PAM) and has a locale setting of LANG=en_US in /etc/environment. A local user named pierre prefers to use the fr_CA locale instead, so he puts export LANG=fr_CA in his .bash_profile. He also puts source ~/.bashrc in that same file, and then puts set +o histexpand in his .bashrc. Now he logs in to the Debian system by sitting at the console. login(1) (via PAM) reads /etc/environment and puts LANG=en_US in the environment. Then login "execs" bash, which reads /etc/profile and .bash_profile. The export command in .bash_profile causes the environment variable LANG to be changed from en_US to fr_CA. Finally, the source command causes bash to read .bashrc, so that the set +o histexpand command is executed for this shell. After all this, pierre gets his prompt and is ready to type commands interactively.

Remote shell logins

Now let's take the second-simplest example: an ssh login. This is extremely similar to the text console login, except that instead of using getty and login to handle the initial greeting and password authentication, sshd(8) handles it. sshd in Debian is also linked with PAM, and it will read the /etc/pam.d/ssh file (instead of /etc/pam.d/login). Otherwise, the handling is the same. Once sshd has run through the PAM steps (if applicable to your system), it "execs" bash as a login shell, which causes it to read /etc/profile and then one of .bash_profile or .bash_login or .profile.

The major difference when using a remote shell login instead of a local console login is that there is a client ssh process running on your local system (or wherever you ssh-ed from) which already has its own environment variables -- and some of those may be sent to the sshd on the system you're logging into. In particular, it is desirable for the LANG and LC_* variables to be preserved by the remote shell. Unfortunately, the configuration files on the server may override them. Getting this set up to work correctly in all cases is tricky. (Here's an example procedure for Debian.)

Remote non login non interactive shells

Bash has a special compile time option that will cause it to source the .bashrc file on non-login, non-interactive ssh sessions. This feature is only enabled by certain OS vendors (mostly Linux distributions). It is not enabled in a default upstream Bash build, and (empirically) not on OpenBSD either.

If this feature is enabled on your system, Bash detects that SSH_CLIENT or SSH_CLIENT2 is in the environment and in this case source .bashrc. For example, suppose you have var=foo in your remote .bashrc and you do ssh remotehost echo \$var it will print foo.

This shell is non-interactive so you can test $- or $PS1, if you don't want things to be executed this way in your .bashrc.

Without this option bash will test if stdin is connected to a socket and will also source .bashrc in this case BUT this test fails if you use a recent openssh server (>5.0) which means that you will probably only see this on older systems.

Note that a test on SHLVL is also done, so if you do: ssh remotehost bash -c echo then the first bash will source .bashrc but not the second one (the explicit bash on the command line that runs echo).

The behaviour of the bash patched to source a system level bashrc by some vendors is left as an exercise.

X sessions

Let's suppose pierre (our console user) decides he wants to run X for a while. He types startx, which invokes whichever set of X clients he prefers. startx is a wrapper around xinit, which runs the X server ("X"), and then runs through pierre's .xinitrc or .xsession file (if either one exists), or the system-wide default Xsession otherwise. Let's suppose pierre has the command exec fluxbox (and nothing else) in his .xsession file. When the smoke clears, he'll have an X server process running (as root), and a fluxbox process running (as pierre). fluxbox was created as a child of xinit, which was a child of startx, which was a child of bash, so it inherited pierre's LANG and other environment variables. When pierre launches something from the window manager's menu, that new command will be a child of fluxbox, so it inherits LANG, PATH, MAIL, etc. as well. In addition to all of that, fluxbox inherits the DISPLAY environment variable which tells it which X server to contact (in this case, probably :0).

So what happens when pierre runs an xterm? fluxbox "forks" and "execs" an xterm process, which inherits DISPLAY, and so on. xterm contacts the X server, authenticates if necessary, and then draws itself on the display. In addition to DISPLAY, it inherited pierre's SHELL variable, which probably contains /bin/bash, so it sets up a pseudo-terminal, then spawns a /bin/bash process to run in it. Since /bin/bash doesn't start with a -, this one will not be a login shell. It will be a normal shell, which doesn't read /etc/profile or .bash_profile or .profile. Instead, it reads .bashrc which in our example contains the line set +o histexpand. So his new xterm is running a bash shell, with all of his environment variables set (remember, they were inherited from his initial text console login shell), and his shell option of choice has been enabled (from .bashrc).

What we've seen so far is the normal Unix setup. Many people choose to run their systems this way, and it works well. It's also fairly simple to understand once you've been exposed to the basic concepts. If you want to change something, you know precisely what file to edit to make it happen -- aliases and transient shell options go in .bashrc, and environment variables, process limits, and so on go in .bash_profile. If you want to run various X client commands before your window manager or desktop environment is invoked (for example, xterm & or xmodmap -e 'keysym Super_R = Multi_key'), you can put them in .xsession before the exec YourWindowManager line.

Display Manager logins

However, some people like to have a graphical login (display manager), and that changes pretty much everything we've seen so far. Instead of getty and login, there's an xdm (or gdm or kdm or wdm or ...) process handling the authentication. And the biggest difference of all is that when our *dm process finishes authenticating the user, it doesn't "exec" a login shell. Instead, it "execs" an X session directly. Therefore, none of the "normal" user configuration files are read in at all -- no /etc/profile, no .bash_profile and no .profile. (But /etc/environment is still read in by PAM, assuming /etc/pam.d/*dm is configured to use pam_limits, as is the case on Debian.)

Let's take xdm as an example. Pierre comes back from vacation one day and discovers that his system administrator has installed xdm on the Debian system. He logs in just fine, and xdm reads his .xsession file and runs fluxbox. Everything seems to be OK until he gets an error message in the wrong locale! Since he overrides the LANG variable in his .bash_profile, and since xdm never reads .bash_profile, his LANG variable is now set to en_US instead of fr_CA.

Now, the naive solution to this problem is that instead of launching xterm, he could configure his window manager to launch xterm -ls. This flag tells xterm that instead of launching a normal shell, it should launch a login shell. Under this setup, xterm spawns /bin/bash but it puts -/bin/bash (or maybe -bash) in the argument vector, so bash acts like a login shell. This means that every time he opens up a new xterm, it will read /etc/profile and .bash_profile (built-in bash behavior), and then .bashrc (because his .bash_profile says to do that). This may seem to work fine at first -- his dot files aren't heavy, so he doesn't even notice the delay -- but there's a more subtle problem. He also launches a web browser directly from his fluxbox menu, and the web browser inherits the LANG variable from fluxbox, which is still set to the wrong locale. So while his xterms may be fine, and anything launched from his xterms may be fine, his web browser is still giving him pages in the wrong locale.

So, what's the best solution to this problem? There really isn't a universal one. One approach is to modify the .xsession file to look something like this:

   1 [ -r /etc/profile ] && source /etc/profile
   2 [ -r ~/.bash_profile ] && source ~/.bash_profile
   3 xmodmap -e 'keysym Super_R = Multi_key'
   4 xterm &
   5 exec fluxbox

This causes the shell that's interpreting the .xsession script to read in /etc/profile and .bash_profile if they exist and are readable, before running xmodmap or xterm or "execing" the window manager. However, there's one potential drawback to this approach: under xdm, the shell that reads .xsession runs without a controlling terminal. If either /etc/profile or .bash_profile uses any commands that assume the presence of a terminal (such as fortune or stty), those commands may fail. This is the primary reason why xdm doesn't read those files by default. If you're going to use this approach, you must make sure that all of the commands in your "dot files" are safe to run when there's no terminal.

One way to do that, but still retain those commands for use when you login with ssh, is to protect the relevant block of code with an if statement. For example:

   1 ## Sample .bash_profile
   2 export PATH=$HOME/bin:$PATH
   3 export MAIL=$HOME/Maildir/
   4 export LESS=-X
   5 export EDITOR=vim VISUAL=vim
   6 export LANG=fr_CA
   7 # Begin protected block
   8 if [ -t 0 ]; then       # check for a terminal
   9   [ x"$TERM" = x"wy30" ] && stty erase ^h       # sample legacy environment
  10   echo "Welcome to Debian, $LOGNAME"
  11   /usr/games/fortune
  12 fi
  13 # End protected block
  14 [ -r ~/.bashrc ] && source ~/.bashrc

Unfortunately, the other display manager programs (kdm, gdm, etc.) do not all use the same configuration files that xdm uses. So this approach may not work for them. You may need to consult the documentation for your display manager program to find out which file(s) you should use for controlling your sessions.

CategoryShell CategoryUnix

DotFiles (last edited 2021-10-23 00:22:04 by emanuele6)