On this page

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=hello creates a shell variable. export MYVAR=hello creates 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 unset to remove a variable from a program’s environment. The env command has a -u flag that unsets a variable for the duration of a single command — for example, env -u HOME ls runs ls with HOME removed from its environment. This is a standard POSIX flag. Useful when a program’s behavior depends on HOME being 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.