Skip to content

105.1 Customize and use the shell environment

Weight: 4

Candidates should be able to customize shell environments to meet users' needs. Candidates should be able to modify global and user profiles.

Objectives

  • Set environment variables (e.g. PATH) at login or when spawning a new shell.
  • Write Bash functions for frequently used sequences of commands.
  • Maintain skeleton directories for new user accounts.
  • Set command search path with the proper directory.

Terms

., source, /etc/bash.bashrc, /etc/profile, env, export, set, unset, ~/.bash_profile, ~/.bash_login, ~/.profile, ~/.bashrc, ~/.bash_logout, function, alias

The shell environment

The shell is the program that reads your commands and passes them to the kernel. On almost every Linux system that shell is Bash (the Bourne Again Shell).

Your shell environment is everything that shapes how the shell behaves for you: its variables, its aliases and its functions. When Bash starts, it runs a few startup scripts that set these up. Some scripts are global (in /etc/, for every user) and some are personal (in your home directory). This page covers all three building blocks first, then which startup files run, and when.

Variables

A variable is a name that holds a value. Giving it a value is called assignment. Reading the value back is called referencing, and you do it with a $ in front of the name:

$ distro=zorinos
$ echo $distro
zorinos

There must be no space around =. With a space, Bash thinks you are running a command:

$ distro =zorinos
-bash: distro: command not found
$ distro= zorinos
-bash: zorinos: command not found

Three rules to remember: no spaces around =, quotes around a value with spaces, and $ only when you read the variable, never when you set it.

Variable names

A name may contain letters (a-z, A-Z), digits (0-9) and the underscore (_). It may not start with a digit, and it may not contain spaces, not even inside quotes. Use an underscore instead:

$ distro_1=zorinos
$ _distro=zorinos
$ 1distro=zorinos
-bash: 1distro=zorinos: command not found
$ "my distro"=zorinos
-bash: my: command not found
$ my_distro=zorinos

By convention, local variables are written in lowercase and environment variables in UPPERCASE. Assign with = and no spaces around it:

$ name=nagato
$ desc='A programmer who enjoys cycling'
$ echo $name
nagato
$ echo $desc
A programmer who enjoys cycling

Variable values

A value can hold letters, digits and most other characters. You need quotes when the value has a space, or a character with a special meaning such as <, > or |:

$ distro=zorin 12.4
-bash: 12.4: command not found
$ distro="zorin 12.4"
$ echo $distro
zorin 12.4
$ distro=>zorin          # no quotes: this only creates an empty file named zorin
$ distro=">zorin"
$ echo $distro
>zorin

Single and double quotes are not the same. Single quotes keep every character exactly as written. Double quotes still replace variables with their values:

$ lizard=uromastyx
$ animal='My $lizard'
$ echo $animal
My $lizard
$ animal="My $lizard"
$ echo $animal
My uromastyx

When you read a variable that holds extra spaces, put double quotes around it, or Bash squeezes the spaces:

$ lizard="     genus   |   uromastyx"
$ echo $lizard
genus | uromastyx
$ echo "$lizard"
     genus   |   uromastyx

Two more small traps. A ! must be the last character, or Bash treats it as a history command. A \ must be written as \\, because a single backslash at the end means "the command continues on the next line":

$ distro=zorin.?/!os
-bash: !os: event not found
$ distro=zorinos\\
$ echo $distro
zorinos\

Read-only variables

readonly makes a variable that cannot be changed, which is useful in scripts:

$ readonly reptile=tortoise
$ reptile=lizard
-bash: reptile: readonly variable

You can also make an existing variable read-only with readonly reptile. Type readonly alone (or readonly -p) to list them all.

Local variables, set and unset

A variable you create with name=value is a local (or shell) variable. It exists only in the shell where you made it.

set with no arguments lists all shell variables and functions, local and environment. The list is long, so use less or grep:

$ reptile=tortoise
$ set | less
BASH=/bin/bash
BASHOPTS=checkwinsize:cmdhist:complete_fullquote:expand_aliases:extglob:extquote:force_fignore:histappend:interactive_comments:login_shell:progcomp:promptvars:sourcepath
BASH_ALIASES=()
BASH_VERSION='4.4.12(1)-release'
(...)
$ set | grep reptile
reptile=tortoise

A local variable is not passed to child processes. When you type bash, you start a new child shell, and the variable is gone there:

$ bash
$ echo $reptile

$ exit

set has a second job: it turns Bash options on and off.

Option Effect
set -b report a background job's end at once, not at the next prompt
set -e exit as soon as a command (or pipeline) fails
set -n read commands but do not run them, to check a script for syntax errors
set -t exit after reading and running one command
set -C stop > from overwriting files that already exist

set -e at the top of a script is common: it makes the script stop at the first failing command instead of carrying on blindly.

unset removes a variable. Give it the name only, without $:

$ echo $reptile
tortoise
$ unset reptile
$ name=nagato
$ unset name
$ echo $name

$ echo $reptile

Environment variables and export

An environment (or global) variable exists in the current shell and in every process started from it. export turns a local variable into an environment variable:

$ reptile=tortoise
$ export reptile
$ bash
$ echo $reptile
tortoise

You can set and export in one step:

$ export amphibian=frog
$ bash
$ echo $amphibian
frog

Another example:

$ export name=nagato
$ echo $name
nagato
$ bash            # start a sub-shell
$ echo $name
nagato            # still there, because it was exported

export -n amphibian turns it back into a local variable. declare -x does the same as export.

   parent shell                      child shell (bash)
   ------------                      ------------------
   reptile=tortoise   (local)   -->  not here
   export amphibian=frog        -->  amphibian=frog

Four commands list the environment variables:

Command Shows
export or export -p every environment variable, as declare -x lines
env every environment variable, as NAME=value
printenv the same as env
printenv PWD the value of one variable (note: no $)
$ env
LANG=en_GB.UTF-8
USER=user2
PWD=/home/user2
HOME=/home/user2
MAIL=/var/mail/user2
SHELL=/bin/bash
SHLVL=1
LOGNAME=user2
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
_=/usr/bin/env
$ printenv PWD
/home/user2

Remember the difference: set shows everything (local, environment and functions), while env, printenv and export show only the environment.

env: run a program in a changed environment

env prints the environment, runs a command with a changed environment, or starts a command with an empty one:

$ env | head -4
SHELL=/bin/bash
HOME=/home/nagato
PATH=/usr/local/bin:/usr/bin:/bin
LANG=en_US.UTF-8
$ env LANG=C sort names.txt      # sort using the C locale, just this once
$ env -u LANG mycommand          # run mycommand with LANG removed
$ printenv HOME
/home/nagato

env can change the environment for one command only:

Option Effect
env -i command run the command with an (almost) empty environment
env -u NAME command run the command without the variable NAME
env NAME=value command run the command with NAME set to value
$ env -i bash
$ echo $USER

$ env
LS_COLORS=
PWD=/home/user2
SHLVL=1
_=/usr/bin/printenv

A useful case is BASH_ENV. Scripts read no normal startup files, but if BASH_ENV names a file, they run that file first. Here a script prints a variable that only exists in .startup_script:

$ cat .startup_script
CROCODILIAN=caiman
$ cat test_env.sh
#!/bin/bash
echo $CROCODILIAN
$ chmod +x test_env.sh
$ env BASH_ENV=/home/user2/.startup_script ./test_env.sh
caiman
$ BASH_ENV=/home/user2/.startup_script ./test_env.sh
caiman

The last line shows that the word env is optional: NAME=value command does the same.

Common environment variables

Variable Holds Example value
HOME your home directory. ~ means the same thing /home/carol
USER your user name carol
SHELL the path of your shell /bin/bash
PWD the current directory /home/user2
HOSTNAME the computer's network name debian
HOSTTYPE the processor architecture x86_64
LANG the system locale en_GB.UTF-8
PATH where Bash looks for commands see below
PS1 the prompt $
PS2 the continuation prompt for multi-line commands >
PS3 the prompt of the select command
PS4 the prefix for debug output +
DISPLAY which X server graphical programs use. Empty means no X :0
HISTFILE the file that stores your command history /home/user2/.bash_history
HISTSIZE how many commands are kept in memory during the session 1000
HISTFILESIZE how many commands are kept in HISTFILE 2000
HISTCONTROL which commands are not saved: ignorespace (lines starting with a space), ignoredups (repeats), ignoreboth ignoreboth
LD_LIBRARY_PATH extra directories to search for shared libraries /usr/local/lib
MAIL the file where Bash checks for your mail /var/mail/carol
MAILCHECK how often, in seconds, Bash checks for mail 60

DISPLAY has the form host:display, and an empty host means this machine. So :0 is the first display of the local machine, and my.xserver:0.1 adds a screen number (display 0, screen 1). history prints HISTFILE, and history 3 prints the last three commands.

PATH, the command search path

When you type a command name, Bash searches the directories listed in PATH, in order, and runs the first match. The directories are absolute paths separated by colons. On Debian, PATH is set in /etc/profile, with a different value for root (user ID 0) and for normal users:

if [ "`id -u`" -eq 0 ]; then
  PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
else
  PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
fi
export PATH

To give every normal user /usr/local/sbin too, add it to the second line. After the next login:

# su - carol
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/usr/local/sbin

For just your current shell, change it on the command line. The position matters, because the first match wins:

$ PATH=/usr/local/sbin:$PATH      # searched FIRST
$ PATH=$PATH:/usr/local/sbin      # searched LAST
$ export PATH="$HOME/bin:$PATH"

Putting $HOME/bin first means your own command is found before a system command of the same name. Set it in ~/.bashrc to make it permanent.

/etc/profile sets PS1 the same way: # for root and $ for everyone else.

Sourcing files: . and source

When you run a script normally, Bash starts a child process, runs the script there, and the child disappears with all its variables. Sourcing a file runs it inside the current shell instead, so its variables, aliases and functions stay. There are two ways to write it, and they are the same:

$ . ~/.bashrc
$ source ~/.bashrc

So source file (short form . file) applies your edits now, with no need to log out.

Startup files use the dot a lot. This block from ~/.profile sources ~/.bashrc if it exists (-f):

# include .bashrc if it exists
if [ -f "$HOME/.bashrc" ]; then
    . "$HOME/.bashrc"
fi

The everyday use: you edit a startup file and want the change now, without logging out:

user2@debian:~$ echo "alias hi='echo We salute you.'" >> ~/.bashrc
user2@debian:~$ tail -n 1 ~/.bashrc
alias hi='echo We salute you.'
user2@debian:~$ . ~/.bashrc
user2@debian:~$ hi
We salute you.

Use >> to append. A single > would overwrite your whole ~/.bashrc.

Aliases

An alias is a short name for a command, or for several commands. The syntax is alias name=command, and you need quotes as soon as the command has a space:

$ alias oldshell=sh
$ alias ls='ls --color=auto'
$ alias ll='ls -al'
$ alias git_info='which git;git --version'
$ git_info
/usr/bin/git
git version 2.7.4

Aliases you often see in ~/.bashrc:

alias ll='ls -alF'
alias la='ls -A'
alias testnet='ping 4.2.2.4'

An alias only expands at the start of a command line, and unalias ll removes one. The semicolon runs several commands in one alias. More things to know:

You type Result
alias lists all aliases
unalias git_info removes an alias
\ls runs the real ls, skipping an alias with the same name
alias my_home=where? an alias can call another alias

An alias with the same name as a real command wins over the command. That is why alias ls='ls --color=auto' works, and why \ls is there when you want the original.

Single or double quotes in an alias

This matters with variables. Single quotes read the variable each time you run the alias. Double quotes read it once, when you create the alias:

$ alias where?='echo $PWD'
$ cd Music
$ where?
/home/user2/Music

$ alias where?="echo $PWD"
$ cd Music
$ where?
/home/user2

The double-quoted alias still prints the directory you were in when you defined it.

Making aliases permanent

An alias typed at the prompt disappears when you close the shell. To keep it, put it in a startup file. The usual place is ~/.bashrc, and most ~/.bashrc files also source a separate file just for aliases:

# part of ~/.bashrc
if [ -f ~/.bash_aliases ]; then
    . ~/.bash_aliases
fi

So you can keep your own aliases in ~/.bash_aliases:

alias git_info='which git;git --version'
alias greet='echo Hello world!'
alias ll='ls -al'
alias where?='echo $PWD'

Functions

A function groups several commands under one name, like an alias, but it can also use arguments, conditions and loops. There are two ways to write one, and both are valid:

function greet {             greet() {
    echo "Hello world!"          echo "Hello world!"
}                            }

You can type a function at the prompt. Bash shows the > prompt (PS2) until you close it. On one line, separate the commands with ;, including after the last one:

$ greet() {
> greeting="Hello world!"
> echo $greeting
> }
$ greet() { greeting="Hello world!"; echo $greeting; }
$ greet
Hello world!

Reach for a function when a task is too big for an alias or needs arguments ($1, $2, and so on):

funnyls () {
    ls -ltrh
    echo "This is a funny ls"
}

Like aliases, functions disappear when the shell closes. To keep them, put them in ~/.bashrc (for you) or /etc/bash.bashrc (for everyone), then source the file.

Special variables

Bash fills in these variables for you. You can read them, but you cannot assign them:

Variable Holds
$? the exit status of the last command. 0 means success, anything else is an error
$$ the process ID (PID) of the current shell
$! the PID of the last background job
$0 the name of the shell or script
$1 to $9 the arguments, in order
$# the number of arguments
$@, $* all the arguments
$_ the last argument of the previous command
$ ps aux |rep bash
-bash: rep: command not found
$ echo $?
127
$ echo $$
420

Inside a function, $1, $2 and so on are the function's own arguments:

$ special_vars() {
> echo $0
> echo $1
> echo $2
> echo $3
> }
$ special_vars debian ubuntu zorin
-bash
debian
ubuntu
zorin

Aliases cannot use arguments this way. They always add them at the end:

$ alias great_editor='echo $1 is a great text editor'
$ great_editor emacs
is a great text editor emacs

That is one of the main reasons to choose a function over an alias.

Functions in files and scripts

Functions usually live in files. Put this in a file named funed:

editors() {
editor=emacs
echo "The text editor of $USER is: $editor."
echo "Bash is not a $1 shell."
}

Source the file, then call the function with an argument:

$ . funed
$ editors tortoise
The text editor of user2 is: emacs.
Bash is not a tortoise shell.

The function used a local variable (editor), an environment variable (USER) and an argument ($1). Add the line #!/bin/bash at the top and a call editors tortoise at the bottom, make the file executable with chmod +x funed.sh, and it becomes a script you run with ./funed.sh.

A function can call another function. For example, an editors function in ~/.bashrc can end with a call to check_vids, a second function, and then both run, in order, at every login.

unset for functions

unset removes functions as well as variables:

Command Removes
unset -v name only the variable name
unset -f name only the function name
unset name the variable if there is one, otherwise the function

This lets an alias contain a function that cleans itself up after running:

$ alias great_editor='gr8_ed() { echo $1 is a great text editor; unset -f gr8_ed; }; gr8_ed'
$ great_editor emacs
emacs is a great text editor

Types of shell

Which startup files run depends on the type of shell. Two questions decide it:

  • Interactive or non-interactive? Interactive means a person types commands and reads the output. A script is non-interactive.
  • Login or non-login? A login shell is the one you get when you log in with a user name and password.
Type Example
interactive login logging in on a text console (Ctrl+Alt+F1 to F6), or with ssh
interactive non-login opening a terminal window in your desktop, or typing bash
non-interactive non-login a script, for example one run by cron. It reads no startup files
non-interactive login rare: bash --login some_script, or piping into ssh

A terminal on a text console is a tty. A terminal window in a graphical desktop is a pts (pseudo terminal). Ctrl+Alt+F7 takes you back to the desktop.

Starting a particular type

bash options:

Command Starts
bash -l, bash --login a login shell
bash -i an interactive shell
bash --noprofile a login shell that skips /etc/profile, ~/.bash_profile, ~/.bash_login and ~/.profile
bash --norc an interactive shell that skips /etc/bash.bashrc and ~/.bashrc
bash --rcfile file an interactive shell that reads file instead of those two

su and sudo start shells as other users. The - (or -l, --login) is what makes it a login shell, with the target user's full environment:

Command Shell
su - user2, su -l user2, su --login user2 interactive login shell as user2
su user2 interactive non-login shell as user2
su -, su - root interactive login shell as root
su, su root interactive non-login shell as root
sudo -i interactive login shell as root
sudo -s, sudo -u root -s non-login shell as root
sudo -u user2 -s interactive non-login shell as user2

To use sudo, a user must be in the sudoers list, for example by joining the sudo group: usermod -aG sudo user2.

Which shell am I in?

echo $0 answers: -bash (or -su) for an interactive login shell, bash (or /bin/bash) for an interactive non-login shell, and the script name inside a script. The leading dash means login shell. You can see it in ps too:

user2@debian:~$ ps aux | grep bash
user2   5270  0.1  0.1  25532  5664 pts/0  Ss  23:03  0:00 bash
user2   5411  0.3  0.1  25608  5268 tty1   S+  23:03  0:00 -bash
user2   5452  0.0  0.0  16760   940 pts/0  S+  23:04  0:00 grep --color=auto bash

The pts/0 shell is a terminal window (non-login, bash). The tty1 shell is a console login (-bash).

Startup files

Global files live in /etc/. Personal files live in your home directory (~). Because personal files run after global ones, personal settings win.

 INTERACTIVE LOGIN SHELL                  INTERACTIVE NON-LOGIN SHELL
 (console, ssh, su -)                     (terminal window, bash)

 /etc/profile                             /etc/bash.bashrc
   |-- runs /etc/profile.d/*                    |
   |-- usually sources /etc/bash.bashrc         v
   v                                      ~/.bashrc
 the FIRST one that exists of:
   ~/.bash_profile
   ~/.bash_login
   ~/.profile  -- usually sources ~/.bashrc

 at logout: ~/.bash_logout                NON-INTERACTIVE (scripts)
                                          only the file named in $BASH_ENV

Interactive login shell

File Level Purpose
/etc/profile global sets variables like PATH and PS1 for all users, runs the scripts in /etc/profile.d/, and usually sources /etc/bash.bashrc
/etc/profile.d/* global extra scripts run by /etc/profile. Packages drop their own settings here
~/.bash_profile personal, Bash only the user's login settings
~/.bash_login personal, Bash only read only if there is no ~/.bash_profile
~/.profile personal, any shell read only if neither of the two above exists, which is the normal case. It sources ~/.bashrc and often adds ~/bin to PATH
~/.bash_logout personal, Bash only runs when you leave a login shell. Often it just clears the screen

The same order as a list:

1. /etc/profile
     -> runs everything in /etc/profile.d/
2. the FIRST one that exists of:
     ~/.bash_profile     (found? stop here)
     ~/.bash_login       (else, found? stop here)
     ~/.profile          (else)
3. ~/.bashrc

Only one of the three personal files is read. Bash tries ~/.bash_profile, then ~/.bash_login, then ~/.profile, and stops at the first one it finds.

Interactive non-login shell

File Level Purpose
/etc/bash.bashrc global settings for all interactive shells (/etc/bashrc on some systems)
~/.bashrc personal your aliases, functions and history settings. Sources ~/.bash_aliases if it exists

~/.bash_profile, ~/.profile and friends are login files, so they are not read here. That is why personal aliases and functions belong in ~/.bashrc: it is read by every interactive shell, and login shells reach it through ~/.profile.

Non-interactive shell

Scripts read no startup files. They only read the file named in BASH_ENV, if that variable is set (on most systems it is not). A non-interactive shell started with -l or --login behaves like a login shell and reads the login files.

Seeing it happen

Add a line to each startup file that says which file it is:

root@debian:~# echo 'echo Hello from /etc/profile' >> /etc/profile
root@debian:~# echo 'echo Hello from ~/.profile' >> /home/user2/.profile
root@debian:~# echo 'echo Hello from /etc/bash.bashrc' >> /etc/bash.bashrc
root@debian:~# echo 'echo Hello from ~/.bashrc' >> /home/user2/.bashrc

A non-login shell reads only the two bashrc files:

user2@debian:~$ bash
Hello from /etc/bash.bashrc
Hello from /home/user2/.bashrc

A login shell reads them all:

root@debian:~# su - user2
Hello from /etc/bash.bashrc
Hello from /etc/profile
Hello from /home/user2/.bashrc
Hello from /home/user2/.profile

Read the order carefully. /etc/profile sourced /etc/bash.bashrc before it finished, and ~/.profile sourced ~/.bashrc before it finished. With su user2 (no dash) you would see only the two bashrc lines. A script run with bash -l gets the login files too:

user2@debian:~$ echo "echo 'Hello from a script'" > test.sh
user2@debian:~$ chmod +x ./test.sh
user2@debian:~$ bash -l ./test.sh
Hello from /etc/profile
Hello from /home/user2/.profile
Hello from a script

The skeleton directory: /etc/skel

When you create a new user (for example with useradd -m or adduser), their home directory starts as a copy of /etc/skel/. So whatever you put there, every new user gets. That is how you give all new users the same startup files.

adduser reads the location from the SKEL variable in its config file:

user2@debian:~$ grep SKEL /etc/adduser.conf
# The SKEL variable specifies the directory containing "skeletal" user
SKEL=/etc/skel

The files start with a dot, so list them with ls -a:

user2@debian:~$ ls -a /etc/skel/
.  ..  .bash_logout  .bashrc  .profile

To give every new user a directory for their scripts, create it in /etc/skel. Here user2 is deleted and created again to get a fresh home directory:

root@debian:/etc/skel# mkdir my_personal_scripts
root@debian:~# deluser --remove-home user2
root@debian:~# adduser user2
Adding user `user2' ...
Adding new group `user2' (1001) ...
Adding new user `user2' (1001) with group `user2' ...
Creating home directory `/home/user2' ...
Copying files from `/etc/skel' ...
(...)
root@debian:~# su - user2
user2@debian:~$ ls -a
.  ..  .bash_history  .bash_logout  .bashrc  my_personal_scripts  .profile

Changes to /etc/skel affect only users created after the change. Existing users are not affected: their home directories are not touched.

$ ls -A /etc/skel
.bash_logout  .bashrc  .profile

Summary

I treat this objective as knowing which file to edit and how a value spreads. My shell environment is made of variables, aliases and functions, and Bash builds it from startup scripts every time it starts. I set a variable with name=value, with no spaces around the =, quotes around values with spaces, and $ only when I read it. Single quotes keep text as written, double quotes expand variables, and readonly locks a value. A plain variable is local to my shell; export makes it an environment variable that sub-shells and child processes inherit, and export -n undoes that. set lists everything including local variables and functions, and also toggles options like set -e (stop a script on the first error) and set -C, while env, printenv and export list only the environment. unset removes a variable, or a function with -f. env -i runs a command with an empty environment and env NAME=value command changes one variable for one command.

PATH is the colon-separated list of directories Bash searches for commands, set for everyone in /etc/profile. I add a directory with PATH=$PATH:/dir to search it last, or prepend it with PATH=/dir:$PATH to search it first. Other variables to know are HOME, USER, SHELL, PS1, DISPLAY, LANG and the history set HISTFILE, HISTSIZE, HISTFILESIZE and HISTCONTROL.

An alias is a short name for a command, removed with unalias and skipped with a leading \. In an alias, single quotes expand variables each time it runs and double quotes freeze them at creation. For anything with arguments I use a function, written as name() { ... } or function name { ... }, which reads its arguments in $1, $2, $# and $@, and $? gives the last exit status. To keep aliases and functions I put them in ~/.bashrc (or ~/.bash_aliases), and I load a changed file with . or source, which run it in the current shell so its settings stick.

Login shells read /etc/profile and /etc/profile.d/, then the first of ~/.bash_profile, ~/.bash_login and ~/.profile, which usually sources ~/.bashrc, and run ~/.bash_logout on the way out. Non-login interactive shells read /etc/bash.bashrc and ~/.bashrc. Non-interactive shells such as scripts read only the file named in BASH_ENV, if it is set. Personal files win because they run last. su - and sudo -i give a login shell, plain su does not, and echo $0 shows a leading dash in a login shell. Finally, /etc/skel is the template copied into every new home directory, so that is where I put files every new user should get.