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:
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:
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":
Read-only variables¶
readonly makes a variable that cannot be changed, which is useful in scripts:
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:
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 $:
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:
You can set and export in one step:
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 |
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:
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:
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):
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:
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:
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:
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):
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 |
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:
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:
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:
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.
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.