The Shell Essentials chapter
covered $HOME, export, and $PATH as tools you use alongside other
commands. This guide goes deeper: how variables behave, why
some survive a script call and others don’t, which files bash reads at
startup, and how to make your configuration stick.
Everything here was tested on bash 5.3 across Ubuntu 26.04, Fedora 44, and Arch Linux. Where behavior differs between distros, you will see it called out.
What environment variables are
An environment variable is a name-value pair that the shell passes to
every program it launches. When you type ls, the kernel inherits a
table of variables from the shell: HOME, PATH, USER, LANG, and
others. Some are set by the system at login; others you set yourself.
The distinction between what stays in the shell and what gets passed
outward is the core concept of this guide.
Reading variables
Three ways to inspect what is set.
env and printenv both print every exported variable,
meaning the ones inherited by child processes. The output is one NAME=value pair
per line:
$ env
SHELL=/bin/bash
HOME=/home/ubuntu
USER=ubuntu
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:...
printenv can also show a single variable:
$ printenv HOME
/home/ubuntu
To read a variable inside the shell itself, use $ expansion with echo:
$ echo $USER
ubuntu
echo $PATH works the same way and is the usual way to inspect your
search path. The $ is what tells bash to substitute the value.
A practical difference: env and printenv only show exported
variables. echo can read any variable, even one you just set without
exporting.
Setting variables — and what export actually does
A bare assignment creates a shell variable. It exists in the current shell but is invisible to any program you launch:
$ MYVAR="hello"
$ echo $MYVAR
hello
$ env | grep MYVAR
That last line produces nothing. MYVAR is not in the environment.
export promotes a variable from shell scope to environment scope.
You can promote an existing variable:
$ export MYVAR
$ env | grep MYVAR
MYVAR=hello
Or combine set and export in one step, which is the more common form:
$ export EDITOR=nano
$ env | grep EDITOR
EDITOR=nano
The difference matters in scripts. A variable set without export is
local to the current shell. If you call a script or a function that
launches subprocesses, those subprocesses won’t see it.
Shell variable vs. environment variable — the quick version: a shell variable lives only in the current shell. An environment variable is a shell variable that has been exported, meaning every child process inherits it.
MYVAR=hellocreates a shell variable.export MYVAR=hellocreates an environment variable.
Unsetting and clearing variables
unset removes a variable entirely, both the shell-local copy and
the environment copy if it was exported:
$ export MYVAR="hello"
$ echo $MYVAR
hello
$ unset MYVAR
$ echo $MYVAR
$ env | grep MYVAR
After unset, the variable is gone. There is no “empty” state
left behind: $MYVAR expands to nothing, and env no longer lists it.
Aside — stripping a variable for one command. You do not always need
unsetto remove a variable from a program’s environment. Theenvcommand has a-uflag that unsets a variable for the duration of a single command — for example,env -u HOME lsrunslswithHOMEremoved from its environment. This is a standard POSIX flag. Useful when a program’s behavior depends onHOMEbeing unset, though that is a rare requirement.
$PATH in depth
$PATH is a colon-separated list of directories. When you type a
command name, bash searches each directory in order, left to right, until
it finds an executable with that name:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Typing ls makes bash look in /usr/local/sbin first (nothing there),
then /usr/local/bin (still nothing), then /usr/sbin or /usr/bin
depending on the distro, where it finally finds ls.
The tool type reveals what bash resolves:
$ type ls
ls is /usr/bin/ls
On Fedora and Arch, the same command shows /usr/sbin/ls. Different
path, same binary. type also identifies builtins:
$ type cd
cd is a shell builtin
which is an external utility that searches $PATH and nothing
else:
$ which grep
/usr/bin/grep
It does not understand builtins, aliases, or functions, so its answer
can differ from what bash actually runs. GNU which may report a
separate binary for a name that is really a builtin or alias, or find
nothing at all, depending on the distro. type shows how bash itself
resolves a command name, which makes it the more reliable tool.
Prepending to PATH
The most common PATH manipulation is adding a directory at the front:
$ export PATH="$HOME/bin:$PATH"
This puts $HOME/bin first in the search order. If you have a custom
script called ls in ~/bin, bash will find it before /usr/bin/ls.
A personal ls shadows the real one for every shell that reads your
PATH — fine when deliberate, surprising when not.
The syntax matters: $PATH on the right side references the old value,
and the whole thing is quoted to handle paths with spaces safely.
Shell startup files — which file runs when
When bash starts, it reads configuration files in a specific order. The
files differ depending on whether the shell is a login shell (first
log in, or ssh into a system) or an interactive non-login shell
(new terminal window in a desktop environment):
- Login shell: reads
/etc/profile, then the first file it finds of~/.bash_profile,~/.bash_login, or~/.profile. - Interactive non-login shell: reads
~/.bashrc.
You can force either mode manually:
$ bash -l # login shell
$ bash -i # interactive shell
Where the distros diverge
Ubuntu does not ship a ~/.bash_profile by default. The default login
file is ~/.profile. Fedora and Arch ship ~/.bash_profile by default.
All three skeleton login files do the same job: they source
~/.bashrc. Ubuntu’s ~/.profile guards the source line, running it
only when bash is running and the file exists (if [ -n "$BASH_VERSION" ]
plus if [ -f "$HOME/.bashrc" ]). Fedora’s ~/.bash_profile sources it
when the file exists (if [ -f ~/.bashrc ]; then . ~/.bashrc; fi).
Arch’s is a one-liner. The whole file, comments and all:
#
# ~/.bash_profile
#
[[ -f ~/.bashrc ]] && . ~/.bashrc
That is why edits to ~/.bashrc show up in login shells too.
On the system side, Ubuntu and Arch have /etc/bash.bashrc. Fedora uses
/etc/bashrc (note: no .bash in the name).
All three distros use /etc/profile.d/, a directory of .sh files
that /etc/profile sources on login. System-wide PATH additions or
umask values often live here.
Reloading your config
After editing a startup file, you do not need to log out and back in.
The source command re-reads the file into the current shell:
$ source ~/.bashrc
The dot command does the same thing and is the POSIX equivalent:
$ . ~/.bashrc
Both are identical in bash. source is easier to read. The dot form
works in any POSIX shell.
Making it stick — practical recipes
Knowing which file runs when lets you place configuration where it takes effect.
A personal PATH addition goes in ~/.bashrc for every new terminal
window, or in ~/.profile (Ubuntu) / ~/.bash_profile (Fedora, Arch)
if you need it only at login. The bashrc version is usually what people
want:
# Add ~/bin to PATH
export PATH="$HOME/bin:$PATH"
One caveat: a variable in ~/.bashrc only reaches shells that read the
file. A cron job never does — cron runs its commands with /bin/sh and
an environment of its own, so nothing you export in ~/.bashrc is
visible to a scheduled job. The
Automation chapter has more on cron.
A custom prompt uses $PS1. This variable controls the prompt
string in interactive shells. Fedora and Arch already set it to a
bracketed format:
$ export PS1='[\u@\h \W]\$ '
The backslash sequences are special: \u is your username, \h is the
hostname, \W is the current directory basename. Put this in
~/.bashrc to make it permanent.
A default editor is another useful export:
export EDITOR=nano
Many terminal-based programs read $EDITOR to decide which text editor
to open — crontab -e, for example, launches whatever this variable
points to. You can also override it for a single command without changing
the variable permanently:
$ EDITOR=nano crontab -e
To make it permanent:
$ echo 'export EDITOR=nano' >> ~/.bashrc
$ source ~/.bashrc
The append puts the export in ~/.bashrc, so every future shell that
reads the file gets it. source re-reads the file now, so the current
shell has it too. Shells that were already open, and programs that
never read ~/.bashrc, see nothing until they read it.
$RANDOM is a bash built-in that returns a random integer between 0
and 32767. It is not an environment variable in the usual sense, since it
generates a new value each time it is expanded:
$ echo $RANDOM
18462
$PWD holds the current working directory, and $OLDPWD holds
the previous one. These are maintained by the shell automatically:
$ pwd
/home/ubuntu/projects
$ cd /tmp
$ echo $OLDPWD
/home/ubuntu/projects
cd - uses $OLDPWD to jump back. Bash maintains these as part of its
current-directory state. You do not set them yourself, but scripts can
read them.
Distro notes
The differences are small but real. A quick reference:
| Ubuntu 26.04 | Fedora 44 | Arch | |
|---|---|---|---|
| Login file | ~/.profile |
~/.bash_profile |
~/.bash_profile |
| Bashrc file | ~/.bashrc |
~/.bashrc |
~/.bashrc |
| System bashrc | /etc/bash.bashrc |
/etc/bashrc |
/etc/bash.bashrc |
| System profile | /etc/profile |
/etc/profile |
/etc/profile |
| Profile.d dir | /etc/profile.d/ |
/etc/profile.d/ |
/etc/profile.d/ |
If you are managing dotfiles across machines, the safest approach is to
put shared settings in ~/.bashrc and login-only settings in a file
that ~/.profile or ~/.bash_profile sources. That way the same
~/.bashrc works everywhere, and each login file handles its own
distro’s convention.
The $SHELL variable itself drifts: on Arch it is
/usr/bin/bash, on Ubuntu and Fedora it is /bin/bash. They point to the same
binary (usually a symlink), but scripts that compare $SHELL to a
hardcoded path should account for both forms.
Where you are now
You know what environment variables are, how to set and export them, and which files bash reads at startup. If you want to put this into practice:
- The Shell Essentials chapter covers pipes, redirection, globbing, and aliases — the everyday building blocks that work alongside variables.
- The Bash Scripting guide
goes deeper into quoting,
set -e/set -u, functions, and writing scripts that behave when something goes wrong.
Other shells handle variables differently. Zsh reads ~/.zshrc instead
of ~/.bashrc and has its own prompt system. Fish does not use export
at all — every variable is exported by default, and set handles both
scopes. The concepts here are bash-specific, though environment variables
as inherited key-value pairs is a POSIX-wide idea.