ZetCode

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:

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

VariableMeaning
HOMEHome directory of the user; ~ expands to it
USER, LOGNAMEName of the user
PATHDirectories searched for commands
SHELLLogin shell of the user (not necessarily the current shell)
PWD, OLDPWDCurrent and previous directory
LANG, LC_*Locale: language, encoding, formats
TZTime zone
TERMTerminal type, used by full-screen programs
EDITOR, VISUALPreferred text editor
PAGERPreferred pager, such as less
TMPDIRDirectory 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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.