Shell Types and Startup Files

Quite often when we want to install some tools or set up an environment, the tutorials will tell us “hey you should add this line to your .bashrc file”, or sometimes “your .bash_profile file”, etc. However, sometimes this works and sometimes it doesn’t. Why? The answer lies in the shell types: there are actually different shell types that affect the way the shell deals with the startup files.

Specifically, there are login shells vs. non-login shells, and interactive shells vs. non-interactive shells. The difference matters for essentially one reason: to determine the startup files and some default options upon a shell’s instantiation (and in reverse, maybe less commonly noticed, cleanup files upon exit). When we tried adding some lines in some files and it didn’t work, we probably have added those lines in the wrong place or we were not using the shell in the right way. So, to make the shell work for us properly, we need to first understand, well, how it works.

Note: this concept applies to most versions of shells, but I’ll give examples for bash shell & zsh shell which I’m most confident about.

The Two Shell Type Dichotomies

As the names suggest, the meanings of the types are pretty straightforward.

Login vs. Non-Login

A login shell is any shell that logs us as a user. In most cases we get a login shell by:

  • opening up a terminal emulator (see here: what terminal emulator is), e.g., Terminal application on our desktop, or tmux from command line
  • ssh onto a machine
  • invoking the shell explicitly with -l or --login option (e.g., bash -l or zsh -l).

Usually we get a prompt for our credentials to login, but it’s not necessarily the case. For example, if we invoke a nested shell from the current shell like this, it won’t ask for the credentials:

dian@ubuntu $ bash -l
dian@ubuntu $ # already in the sub shell

A non-login shell, on the contrary, is a shell that’s not obtained in the above mentioned ways. Some examples include:

  • executing a shell script or a command string, e.g., bash my_script.sh or bash -c 'echo hello'
  • invoking a shell without -l or --login option.

Interactive vs. Non-Interactive

An interactive shell is even more self-explanatory: it asks for user input and immediately writes output to the user’s terminal (unless redirected). On the contrary, a non-interactive shell is usually given a script or a command string to execute, without the need to bother the users for extra input. Although there are ways to make the shell act outside this rule, 99% of the time if we are typing commands to a shell prompt, we are facing an interactive shell; if we are executing shell scripts or command strings, we are using a non-interactive shell.

For the sake of completeness, if we want to ask for an interactive shell no matter what, we can achieve that by explicitly specifying -i option (for zsh --interactive also works).

Four Combinations

One important point to make is that, being login or not has nothing to do with being interactive or not; these two properties are not mutually exclusive. This means that we can effectively have four different combinations of shell types, namely: login interative, login non-interactive, non-login interactive and non-login non-interactive.

Here are some concrete examples for each of these type (the same also applies to zsh interchangeably):

  Login Non-Login
Interactive most common:
1. ssh onto a machine
2. open Terminal or tmux
3. invoke a sub shell using bash -l
nested (sub) shell created with:
1. bash, zsh, etc.
Non-Interactive very rare. can do with:
1. bash -lc <commands>
2. bash -l <scripts>
nested (sub) shell created with:
1. bash -c <commands>
2. bash <scripts>

How Do I Know If It’s a …

Login Shell?

For bash shell, the most reliable way (there are other ways that test environment variables such as $0 but they are not always consistent) is to use the shell built-in command shopt which can show the configurations the shell is currently using:

dian@ubuntu $ shopt login_shell # 0) original login, interative shell
login_shell    	on
dian@ubuntu $ bash              # 1) enters a sub shell, which is non-login, interactive
dian@ubuntu $ shopt login_shell
login_shell    	off
dian@ubuntu $ exit
exit
dian@ubuntu $ # back in the original shell
dian@ubuntu $ bash -l           # 2) enters a sub shell, which is login, interactive
dian@ubuntu $ shopt login_shell
login_shell     on
dian@ubuntu $ exit
logout
dian@ubuntu $ # back in the original shell
dian@ubuntu $ bash -c 'shopt login_shell'   # 3) execute command in a sub shell, which is non-login, non-interactive
login_shell    	off
dian@ubuntu $ bash -cl 'shopt login_shell'  # 4) execute command in a sub shell, which is login, non-interactive
login_shell    	on
dian@ubuntu $ 

For zsh shell, it also provides a built-in mechanism (see here) to test the login property. Specifically, we can use the following scripting:

if [[ -o login ]]; then
  print yes
else
  print no
fi

This is typical zsh style of testing things. Here the testing part goes in [[ ... ]], and -o tells the shell to test the login option. We can see this works in the same context as bash above, with an equivalent one-liner [[ -o login ]] && echo 'yes' || echo 'no':

dian@ubuntu % [[ -o login ]] && echo 'yes' || echo 'no' # 0) original login, interative shell
yes
dian@ubuntu % zsh           # 1) enters a sub shell, which is non-login, interactive
dian@ubuntu % [[ -o login ]] && echo 'yes' || echo 'no'
no
dian@ubuntu % exit
dian@ubuntu % # back in the original shell
dian@ubuntu % zsh -l        # 2) enters a sub shell, which is login, interactive
dian@ubuntu % [[ -o login ]] && echo 'yes' || echo 'no'
yes
dian@ubuntu % exit
dian@ubuntu % # back in the original shell
dian@ubuntu % zsh -c "[[ -o login ]] && echo 'yes' || echo 'no'"    # 3) execute command in a sub shell, which is non-login, non-interactive
no
dian@ubuntu % zsh -cl "[[ -o login ]] && echo 'yes' || echo 'no'"   # 4) execute command in a sub shell, which is login, non-interactive
yes
dian@ubuntu % 

Interactive Shell?

For bash shell, there will be an i character in the $- environment variable (which stores a set of options for the current shell) if it’s an interactive shell, and no i otherwise:

dian@ubuntu $ echo $-   # 0) original login, interactive shell
himBHs
dian@ubuntu $ bash      # 1) enters a sub shell, which is non-login, interactive
dian@ubuntu $ echo $-
himBHs
dian@ubuntu $ exit
exit
dian@ubuntu $ # back in the original shell
dian@ubuntu $ bash -c 'echo $-'     # 2) execute command in a sub shell, which is non-login, non-interactive
hBc
dian@ubuntu $ bash -ci 'echo $-'    # 3) execute command in a sub shell, which is non-login, interactive
himBHc

For zsh shell, the same method also applies (try to test it with $- variable yourself!), but a more native way to find out the interactiveness is to use zsh’s [[ -o interactive ]] testing, similar to [[ -o login ]] as above:

dian@ubuntu % [[ -o interactive ]] && echo 'yes' || echo 'no'   # 0) original login, interative shell
yes
dian@ubuntu % zsh       # 1) enters a sub shell, which is non-login, interactive
dian@ubuntu % [[ -o interactive ]] && echo 'yes' || echo 'no'
yes
dian@ubuntu % exit
dian@ubuntu % # back in the original shell
dian@ubuntu % zsh -c "[[ -o interactive ]] && echo 'yes' || echo 'no'"  # 2) execute command in a sub shell, which is non-login, non-interactive
no
dian@ubuntu % zsh -ci "[[ -o interactive ]] && echo 'yes' || echo 'no'" # 3) execute command in a sub shell, which is non-login, interactive
yes
dian@ubuntu % 

General Guidelines

Generally speaking, a login shell is the first shell we get when we log on a system, or when we explicitly specify the -l or --login option; a non-login shell is anything otherwise, such as the sub shells executed from the initial login shell. An interactive shell is one that we interact with by typing commands, while a non-interactive shell usually accepts a script or a command string and execute them for us. There are ways to override these rules, such as using -l, --login, -i and --interactive flags as mentioned above; in fact, a weird enough example is:

dian@ubuntu $ bash -cil <commands>

which can be tested to be a login, interactive shell at the same time even though it has nothing much to do with logging users in or being interactive whatsoever! But, as unnatural as it feels, we should avoid using shells in these ways. Use them as what they intend to be.

Startup Files

What’s the point of having these different shell types? They are used to determine the startup files to use upon a shell’s instantiation, and the cleanup files to use when it’s going to exit. This enables us to have different routines for different shell purpose. For example, we might want to use a certain environment variable with a login shell while in a non-interactive shell we might want another environment variable to be available.

System-Wide vs. User-Level Startup Files

For bash shell, the commonly seen startup files are: /etc/profile/, /etc/bash.bashrc, ~/.bash_profile, ~/.bash_login, ~/.profile and ~/.bashrc, etc. They can be grouped into two categories: ones that are intended for all users (system-wide) and reside in the /etc/ directory, and ones that are user-customized (user-level) which typically sit in the $HOME directory. The rules of which shell uses what files are complicated (see the official reference for a complete description of behaviors), but generally the user-level startup files are executed after the system-wide files. The most commonly seen cases for us are (3 out of 4):

  • interactive login shell: it will first execute /etc/profile; then it looks for ~/.bash_profile, ~/.bash_login and ~/.profile by order, and executes only the first one upon discovery. The reason behind this lookup is that different Linux/UNIX distributions will have different startup files in place (for example ~/.bash_profile is present on my mac while it’s not on Ubuntu).
  • interactive non-login shell: it will first execute /etc/bash.bashrc; then it will look for ~/.bashrc. However, it’s common to also put a line like if [ -f ~/.bashrc ]; then . ~/.bashrc; fi in ~/.bash_profile so that ~/.bashrc can also be executed in turn in a login shell.
  • non-interactive shell: it is usually not intended to execute any startup files, but can do if $BASH_ENV variable is provided.

For zsh shell, the similar rules apply and there are even more possible startup files (notice the system-wide version is paired with the user-level version):

  • /etc/zshenv: always run for every shell
  • ~/.zshenv: usually run for every shell
  • /etc/zprofile: run for login shells
  • ~/.zprofile: run for login shells
  • /etc/zshrc: run for interactive shells
  • ~/.zshrc: run for interactive shells
  • /etc/zlogin: run for login shells
  • ~/.zlogin: run for login shells

The best way to determine the exact routine is to test it out on your machine. Here, we can see the flexibility with these different types of shells. However, as I’ve warned for a thousand times, even though we can make the shell to execute the startup files we want by forcing weird flag combinations, it’s better to stick with the conventions.

Now we should see, for example, why sometimes a line added to our ~/.bashrc doesn’t work. Because normally only an interactive, non-login shell will see this line and this can be probably extended to an interactive, login shell by “sourcing” (see source command here) ~/.bashrc inside ~/.bash_profile. So, next time when we need to add a customized setting into our startup files, we need to add it in the right place or chain the startup files properly.

Logout Files

For the sake of completeness, there are also optional cleanup files, or more precisely, logout files that do some cleanup chores for us upon logout (they are mostly intended for login shells). For bash shell, there is ~/.bash_logout and for zsh shell, there are ~/.zlogout and /etc/zlogout which are nicely paired and will be execute in this order (opposite to startup).

Summary

To summarize:

  • There are four types of shells: login shells vs. non-login shells, and interactive shells vs. non-interactive shells. They can be further combined into four exact types.
  • We’ve discussed how to find the shell types in detail.
  • Different types of shells have different startup (possibly logout) file routines.
  • Better to stick with good conventions than to force shells into weird ways even though possible.