Linux Environment Variables
last modified October 23, 2026
Environment variables are name-value pairs that every process carries. A
process receives a copy of its parent's environment when it starts, and
passes a copy on to its own children. Programs read them for
configuration: PATH tells the shell where to look for
commands, HOME where the user's files are, LANG
which language and character set to use, and applications read their own
variables for settings such as ports and log levels.
The examples were run in Bash 5.2 on Debian 12, in a login session of the
user jano, started with su - jano.
Listing the environment
The env command, run without arguments, prints the whole
environment.
env | sort
A fresh login session has only a dozen variables. HOME,
USER, LOGNAME, and SHELL come from
the account in /etc/passwd; PATH is set by the
login program; LANG comes from
/etc/default/locale; PWD, OLDPWD, and
SHLVL are maintained by the shell; and _ holds the
last argument of the previous command, here the path of
env. A desktop session has many more.
$ env | sort HOME=/home/jano LANG=en_US.UTF-8 LOGNAME=jano MAIL=/var/mail/jano OLDPWD=/home/jano PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games PWD=/home/jano/work SHELL=/bin/bash SHLVL=1 TERM=xterm-256color USER=jano _=/usr/bin/env
Reading single variables
The printenv command prints the values of the given
variables. In the shell, a variable is expanded with a dollar sign.
printenv HOME printenv USER SHELL echo "$HOME is the home of $USER" printenv NOSUCHVAR; echo $?
printenv exits with status 1 when a variable is not set, so it
can be used in tests. Put expansions in double quotes, so values with
spaces or wildcard characters stay intact.
$ printenv HOME /home/jano $ printenv USER SHELL jano /bin/bash $ echo "$HOME is the home of $USER" /home/jano is the home of jano $ printenv NOSUCHVAR; echo $? 1
Shell variables and export
An assignment in the shell creates a shell variable, which exists only in
the shell itself. The export command marks it for the
environment, so that child processes get a copy.
GREETING=hello echo $GREETING bash -c 'echo "child: $GREETING"' printenv GREETING export GREETING bash -c 'echo "child: $GREETING"' declare -p GREETING
Before export, the child shell started with
bash -c sees an empty variable, and printenv,
also a child process, prints nothing. After export, both see
the value. declare -p shows the -x attribute of an
exported variable. Assignment and export can be combined:
export GREETING=hello.
$ GREETING=hello $ echo $GREETING hello $ bash -c 'echo "child: $GREETING"' child: $ printenv GREETING $ export GREETING $ bash -c 'echo "child: $GREETING"' child: hello $ declare -p GREETING declare -x GREETING="hello"
Setting a variable for one command
Assignments placed before a command set variables in the environment of that command only.
date TZ=Asia/Tokyo date echo "TZ is '$TZ'" printf 'b\nB\na\nA\n' | sort printf 'b\nB\na\nA\n' | LC_ALL=C sort
The TZ variable changes the time zone of date,
and LC_ALL=C makes sort compare raw bytes instead
of using the rules of the locale, which puts uppercase letters first. The
shell's own variables are not changed: TZ is still unset
afterwards.
$ date Fri Oct 23 07:51:27 PM UTC 2026 $ TZ=Asia/Tokyo date Sat Oct 24 04:51:27 AM JST 2026 $ echo "TZ is '$TZ'" TZ is '' $ printf 'b\nB\na\nA\n' | sort a A b B $ printf 'b\nB\na\nA\n' | LC_ALL=C sort A B a b
A common pitfall
A prefix assignment is in effect for the command, but not for the expansion of the command's arguments.
NAME=Jan echo "Hello $NAME" NAME=Jan bash -c 'echo "Hello $NAME"'
In the first command, the shell expands $NAME before running
echo, when NAME is not yet set. In the second,
the single quotes delay the expansion to the child shell, which has the
variable in its environment.
$ NAME=Jan echo "Hello $NAME" Hello $ NAME=Jan bash -c 'echo "Hello $NAME"' Hello Jan
Assignment syntax
There must be no spaces around the equals sign, and values with spaces must be quoted.
COLOR = blue MESSAGE=hello world MESSAGE="hello world" echo "$MESSAGE"
With spaces, the shell treats COLOR as a command with the
arguments = and blue. Without quotes,
MESSAGE=hello becomes a prefix assignment for the command
world. Variable names are case-sensitive. By convention,
environment variables are uppercase, and variables private to a script
lowercase, which avoids clashes with variables such as PATH
or USER.
$ COLOR = blue bash: COLOR: command not found $ MESSAGE=hello world bash: world: command not found $ MESSAGE="hello world" $ echo "$MESSAGE" hello world
Removing variables
The unset command removes a variable; export -n
removes only the export attribute.
export APP_MODE=debug bash -c 'echo "mode: $APP_MODE"' export -n APP_MODE bash -c 'echo "mode: $APP_MODE"' echo $APP_MODE unset APP_MODE echo "[$APP_MODE]"
After export -n, the variable is still a shell variable, but
children no longer get it. After unset, it is gone. Note that
an unset variable and a variable set to an empty string both expand to
nothing.
$ export APP_MODE=debug $ bash -c 'echo "mode: $APP_MODE"' mode: debug $ export -n APP_MODE $ bash -c 'echo "mode: $APP_MODE"' mode: $ echo $APP_MODE debug $ unset APP_MODE $ echo "[$APP_MODE]" []
The env command
With a command, env runs it in a modified environment. The
-i option starts with an empty environment, -u
removes a variable, and NAME=value arguments add
variables.
env -i bash -c env env -i PATH=/usr/bin MODE=test bash -c env env -u HOME bash -c 'echo "HOME=[$HOME]"' env MODE=test printenv MODE
With an empty environment, Bash sets only PWD,
SHLVL, and _ itself. Running a program with
env -i and a few chosen variables tests whether it depends on
something in our environment, a common cause of "works for me" problems,
for example in cron jobs, which run with a minimal environment.
env is also used in shebang lines,
#!/usr/bin/env python3, to find an interpreter in the
PATH.
$ env -i bash -c env PWD=/home/jano/work SHLVL=0 _=/usr/bin/env $ env -i PATH=/usr/bin MODE=test bash -c env PWD=/home/jano/work MODE=test SHLVL=0 PATH=/usr/bin _=/usr/bin/env $ env -u HOME bash -c 'echo "HOME=[$HOME]"' HOME=[] $ env MODE=test printenv MODE test
The PATH variable
PATH is a colon-separated list of directories. When we type a
command without a slash, the shell searches these directories in order and
runs the first match.
echo "$PATH" | tr ':' '\n' type ls date cd which date type -a echo
The type command tells how the shell interprets a name: as a
file found in the PATH, an alias, a function, or a builtin
like cd, which does not use the PATH at all.
Hashed means that Bash remembers where it found the command.
which only searches the PATH.
type -a lists all matches; echo is both a builtin
and a program, and the builtin wins. On Debian 12, /bin is a
link to /usr/bin, so programs appear in both.
$ echo "$PATH" | tr ':' '\n' /usr/local/bin /usr/bin /bin /usr/local/games /usr/games $ type ls date cd ls is /usr/bin/ls date is hashed (/usr/bin/date) cd is a shell builtin $ which date /usr/bin/date $ type -a echo echo is a shell builtin echo is /usr/bin/echo echo is /bin/echo
Adding a directory to PATH
Personal scripts are usually kept in ~/bin or
~/.local/bin. To run them by name, add the directory to the
PATH.
mkdir -p ~/bin printf '#!/bin/sh\necho "Hello from greet"\n' > ~/bin/greet chmod +x ~/bin/greet greet export PATH="$HOME/bin:$PATH" greet type greet
The new value is built from the old one, so nothing is lost. Putting the
directory first gives our scripts priority; appending it,
PATH="$PATH:$HOME/bin", gives priority to the system. The
change lasts only for this shell; to make it permanent, put the line in
~/.profile. Debian's default ~/.profile already
adds ~/bin and ~/.local/bin if they exist at
login.
$ mkdir -p ~/bin $ printf '#!/bin/sh\necho "Hello from greet"\n' > ~/bin/greet $ chmod +x ~/bin/greet $ greet bash: greet: command not found $ export PATH="$HOME/bin:$PATH" $ greet Hello from greet $ type greet greet is hashed (/home/jano/bin/greet)
PATH order and the command hash
The first match in the PATH wins, which can hide a system
command. Bash also caches the locations of commands it has run.
date printf '#!/bin/sh\necho "this is not the real date"\n' > ~/bin/date chmod +x ~/bin/date date type date hash -r date type -a date
After creating ~/bin/date, which comes first in the
PATH, Bash still runs /usr/bin/date from its hash
table. hash -r clears the table, and the next
date finds our script. Assigning to PATH also
clears it. This shadowing is also why the current directory,
., must never be in the PATH of root: a
malicious ls in some directory would run instead of the real
one.
$ date Fri Oct 23 07:51:31 PM UTC 2026 $ printf '#!/bin/sh\necho "this is not the real date"\n' > ~/bin/date $ chmod +x ~/bin/date $ date Fri Oct 23 07:51:31 PM UTC 2026 $ type date date is hashed (/usr/bin/date) $ hash -r $ date this is not the real date $ type -a date date is /home/jano/bin/date date is /usr/bin/date date is /bin/date
A broken PATH
Overwriting PATH instead of extending it makes most commands
unavailable.
PATH=~/bin; ls PATH=~/bin; /bin/ls -d /etc
Both lines were run in a subshell, so the change did not persist. With
the broken PATH, ls is not found, but commands
can still be run with their full path. In an interactive shell, fix it
with export PATH=/usr/local/bin:/usr/bin:/bin or open a new
terminal.
$ PATH=~/bin; ls bash: ls: command not found $ PATH=~/bin; /bin/ls -d /etc /etc
Children cannot change the parent's environment
The environment is copied to a child process; changes in the child never flow back.
cat setenv.sh bash setenv.sh echo "after bash: [$DB_HOST]" source setenv.sh echo "after source: [$DB_HOST]"
Run with bash, the script is a separate process; its
export affects only itself and its children. The
source command (or its synonym .) runs the
script in the current shell, so the variable stays. This is how
~/.profile and Python's activate scripts work,
and why a script cannot cd the shell that started it.
$ cat setenv.sh #!/bin/bash export DB_HOST=db.example.com echo "inside the script: $DB_HOST" $ bash setenv.sh inside the script: db.example.com $ echo "after bash: [$DB_HOST]" after bash: [] $ source setenv.sh inside the script: db.example.com $ echo "after source: [$DB_HOST]" after source: [db.example.com]
Default values in scripts
Parameter expansion provides default values for variables that may not be set.
cat config.sh
bash config.sh
LOG_LEVEL=debug PORT=9000 bash config.sh
unset DB_URL; bash -c 'echo "${DB_URL:?DB_URL is not set}"'
${VAR:-default} expands to the default when the variable is
unset or empty. This lets users configure a script through the environment
without editing it. ${VAR:?message} stops the script with an
error when a required variable is missing. Related forms are
${VAR:=default}, which also assigns the default, and
${VAR:+alt}, which expands to alt only when the
variable is set.
$ cat config.sh
#!/bin/bash
log_level=${LOG_LEVEL:-info}
port=${PORT:-8080}
editor=${EDITOR:-nano}
echo "log level: $log_level"
echo "port: $port"
echo "editor: $editor"
$ bash config.sh
log level: info
port: 8080
editor: nano
$ LOG_LEVEL=debug PORT=9000 bash config.sh
log level: debug
port: 9000
editor: nano
$ unset DB_URL; bash -c 'echo "${DB_URL:?DB_URL is not set}"'
bash: line 1: DB_URL: DB_URL is not set
Loading variables from a file
Applications often keep their settings in .env files with
NAME=value lines.
cat app.env source app.env bash -c 'echo "child sees: [$APP_PORT]"' set -a; source app.env; set +a bash -c 'echo "child sees: [$APP_PORT]"'
Sourcing the file creates shell variables, which child processes do not
see. With set -a, every variable assigned is exported
automatically; set +a turns this off again. Since sourcing
executes the file as shell code, only load files you trust, and quote
values that contain spaces or special characters.
$ cat app.env APP_NAME=inventory APP_PORT=8080 APP_DEBUG=false $ source app.env $ bash -c 'echo "child sees: [$APP_PORT]"' child sees: [] $ set -a; source app.env; set +a $ bash -c 'echo "child sees: [$APP_PORT]"' child sees: [8080]
The environment of a running process
The environment that a process started with can be read from
/proc/PID/environ, where the entries are separated by null
bytes.
REGION=eu-west sleep 60 & tr '\0' '\n' < /proc/$!/environ | grep -E 'REGION|USER' export STAGE=prod tr '\0' '\n' < /proc/$!/environ | grep STAGE echo $? kill $!
$! is the PID of the last background process. The process got
REGION and the inherited variables. A variable exported later
in the shell does not reach a process that is already running. This is why
services must be restarted after a change in their environment. Only the
owner of a process and root can read the file, as it may contain secrets.
$ REGION=eu-west sleep 60 & $ tr '\0' '\n' < /proc/$!/environ | grep -E 'REGION|USER' REGION=eu-west USER=jano $ export STAGE=prod $ tr '\0' '\n' < /proc/$!/environ | grep STAGE $ echo $? 1 $ kill $!
Startup files
Variables set on the command line are lost when the shell exits. Permanent
settings go into startup files, which Bash reads depending on how it was
started. A login shell reads /etc/profile and
~/.profile (or ~/.bash_profile if it exists); an
interactive non-login shell, such as a new terminal window, reads
~/.bashrc.
echo 'export FROM_PROFILE=yes' >> ~/.profile echo 'export FROM_BASHRC=yes' >> ~/.bashrc head -9 ~/.bashrc bash -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"' bash -l -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"' bash -i -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"' bash -l -i -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"'
A non-interactive shell running a command reads neither file. A login
shell (-l) reads ~/.profile; Debian's
~/.profile also sources ~/.bashrc, but the
case at the top of ~/.bashrc returns immediately
when the shell is not interactive, so the line we appended is not reached.
An interactive shell (-i) reads ~/.bashrc.
The rule of thumb: put environment variables in ~/.profile,
where they are set once at login and inherited by everything, and
aliases, functions, and prompt settings in ~/.bashrc. Changes
to ~/.profile take effect at the next login. The last two
commands ran in a terminal, as interactive shells need one.
$ echo 'export FROM_PROFILE=yes' >> ~/.profile
$ echo 'export FROM_BASHRC=yes' >> ~/.bashrc
$ head -9 ~/.bashrc
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
$ bash -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"'
profile= bashrc=
$ bash -l -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"'
profile=yes bashrc=
$ bash -i -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"'
profile= bashrc=yes
$ bash -l -i -c 'echo "profile=$FROM_PROFILE bashrc=$FROM_BASHRC"'
profile=yes bashrc=yes
System-wide settings
Variables for all users are set in several places:
/etc/environment: simpleNAME=valuelines, read by PAM at login for every kind of session; no shell syntax and no variable expansion./etc/profileand files in/etc/profile.d/: shell code run by login shells./etc/default/locale: the language settings, read by PAM on Debian and Ubuntu.Environment=lines in systemd unit files: the environment of services, which do not read any of the files above.
Environment variables and sudo
sudo runs commands with a reset environment, so variables
from our shell do not reach them by default.
export API_TOKEN=abc123 sudo printenv API_TOKEN; echo $? sudo --preserve-env=API_TOKEN printenv API_TOKEN sudo printenv HOME USER
The env_reset default of sudo keeps only a few
safe variables, such as TERM, and sets HOME,
USER, and others for the target user.
--preserve-env passes chosen variables through, if the
security policy allows it. See the
sudo command article for details.
$ export API_TOKEN=abc123 $ sudo printenv API_TOKEN; echo $? 1 $ sudo --preserve-env=API_TOKEN printenv API_TOKEN abc123 $ sudo printenv HOME USER /root root
Language and locale variables
The locale variables control the language of messages, character encoding, sorting, and the format of numbers and dates.
cat /etc/default/locale echo $LANG locale | head -4
LANG sets the default for all categories. Individual
LC_* variables, such as LC_TIME or
LC_NUMERIC, override single categories, and
LC_ALL overrides everything. The locale command
shows the resulting settings.
$ cat /etc/default/locale LANG=en_US.UTF-8 $ echo $LANG en_US.UTF-8 $ locale | head -4 LANG=en_US.UTF-8 LANGUAGE= LC_CTYPE="en_US.UTF-8" LC_NUMERIC="en_US.UTF-8"
Common environment variables
| Variable | Meaning |
|---|---|
HOME | Home directory of the user; ~ expands to it |
USER, LOGNAME | Name of the user |
PATH | Directories searched for commands |
SHELL | Login shell of the user (not necessarily the current shell) |
PWD, OLDPWD | Current and previous directory |
LANG, LC_* | Locale: language, encoding, formats |
TZ | Time zone |
TERM | Terminal type, used by full-screen programs |
EDITOR, VISUAL | Preferred text editor |
PAGER | Preferred pager, such as less |
TMPDIR | Directory for temporary files |
Source
Bash manual: Environment, environ(7) manual page
In this article, we have listed and read environment variables, exported
shell variables, set variables for single commands, worked with the
PATH, used defaults and .env files in scripts,
and seen where permanent settings belong. See also the
echo and
redirection articles.
Author
List all Linux tutorials.