Environment Variables

An environment variable is a named piece of data that lives outside your scripts and programs, attached to a running process, and passed down automatically to every child process that process starts. Environment variables are how Linux tells programs where your home directory is, which text editor to launch, where executables live on disk, and countless other pieces of configuration — all without hardcoding any of it into the programs themselves. Understanding how they are set, inherited, and scoped is essential for configuring your shell, writing portable scripts, and debugging why a program mysteriously can’t find something that clearly exists on disk.

Overview: How Environment Variables Work

Every running process on Linux carries an environment: a list of KEY=value strings the kernel stores alongside the process’s other state (its open files, memory mappings, and so on). When a process starts another process — for example, when your shell runs a command — it does so with the fork() and execve() system calls. fork() duplicates the calling process, including a copy of its environment array; execve() then replaces that child’s program code with the new program, but the environment array rides along unchanged unless the caller edited it first. This is the entire mechanism behind "inheriting" environment variables: a child process gets a private copy of whatever key/value pairs its parent had marked for export, at the exact moment it was created. Nothing stays linked afterward — if the child changes a variable, the parent never sees it, and vice versa.

Bash keeps two related but distinct kinds of variables. A plain shell variable (created with NAME=value) exists only in the current shell’s memory; it is never placed in the environment array, so no child process — not even a subshell started with bash -c — can see it. Running export NAME=value flags that variable for inclusion in the environment of every process this shell subsequently starts. That is the entire difference between a shell variable and an environment variable: export is the switch that copies a name from the shell’s private table into the table handed to children.

You can inspect a live process’s environment directly from the kernel’s /proc filesystem: /proc/PID/environ contains the raw, NUL-separated KEY=value list the kernel gave that process at exec time. The env and printenv commands are friendlier front ends that read the current shell’s exported variables. set, by contrast, lists every shell variable — exported or not — plus shell functions, which is why its output is usually far longer than env‘s.

One environment variable deserves special mention because nearly everything depends on it: PATH. It is a colon-separated list of directories. When you type a bare command name like ls, the shell does not search the whole filesystem — it walks PATH left to right, checking each directory for an executable file with that name, and runs the first match it finds (bash also caches results in a hash table for speed; see hash -r if you need to clear that cache after installing a new binary). If a directory isn’t listed in PATH, a command living there must be invoked with an explicit path, such as ./script.sh or /opt/tool/bin/tool.

Common predefined environment variables you’ll meet constantly:

Variable Meaning
HOME Your home directory, e.g. /home/ada
USER Your login username
SHELL Path to your default login shell, e.g. /bin/bash
PATH Colon-separated directories searched for executables
PWD The shell’s current working directory
OLDPWD The previous working directory, used by cd -
LANG Default locale for language and character encoding
TERM Terminal type; affects how programs draw to the screen
EDITOR Preferred text editor, used by tools like crontab -e

Syntax

The general forms for working with variables and the environment:

export VARIABLE_NAME=value
VARIABLE_NAME=value
unset VARIABLE_NAME
echo $VARIABLE_NAME
env
printenv VARIABLE_NAME
  • export VARIABLE_NAME=value — creates or updates a variable and marks it for inheritance by child processes.
  • VARIABLE_NAME=value — creates or updates a plain shell variable, visible only in the current shell.
  • unset VARIABLE_NAME — removes a variable entirely, from both the shell and (if exported) the environment.
  • echo "$VARIABLE_NAME" — prints the variable’s current value (quoted, to avoid word-splitting on the result).
  • env — lists every variable in the current environment, one KEY=value pair per line.
  • printenv VARIABLE_NAME — prints the value of a single named variable; with no argument it behaves like env.

Examples

Example 1: Inspecting and setting variables

echo "$HOME"
echo "$SHELL"
export EDITOR="nano"
echo "$EDITOR"

Output:

/home/ada
/bin/bash
nano

The first two lines print variables Bash already had set for you at login. The third line creates a new environment variable, EDITOR, and exports it in one step; the fourth confirms it took effect. Any program you launch from this shell from now on — including crontab -e or git commit without -m — will open nano when it needs an editor.

Example 2: Environment variables and child-process inheritance

GREETING="Hello from the parent shell"
bash -c 'echo "$GREETING"'

Output:

Nothing is printed, because GREETING was never exported — it’s a shell variable local to the interactive shell, and the child bash -c process starts with its own separate environment that doesn’t include it. Now export it and repeat:

export GREETING="Hello from the parent shell"
bash -c 'echo "$GREETING"'

Output:

Hello from the parent shell

This time the child process’s environment array includes GREETING, because export flagged it before the child was forked.

Example 3: Extending PATH so a script can be run by name

mkdir -p "$HOME/scripts"
echo '#!/usr/bin/env bash' > "$HOME/scripts/hello.sh"
echo 'echo "Hello, environment!"' >> "$HOME/scripts/hello.sh"
chmod +x "$HOME/scripts/hello.sh"
export PATH="$HOME/scripts:$PATH"
hello.sh

Output:

Hello, environment!

The script is created with > (which truncates or creates the file fresh) and a second line appended with >> (which adds to the end without erasing the first line). After marking it executable and prepending $HOME/scripts to PATH, the shell can find hello.sh by name alone, without a leading ./ or a full path, because it now appears in one of the directories the shell searches.

Example 4: Giving a script a configurable default via an environment variable

#!/usr/bin/env bash
set -euo pipefail

target_env="${DEPLOY_ENV:-staging}"
echo "Deploying application to the ${target_env} environment..."

Output (run normally):

chmod +x deploy.sh
./deploy.sh
Deploying application to the staging environment...

Output (with the variable set inline):

DEPLOY_ENV=production ./deploy.sh
Deploying application to the production environment...

The ${DEPLOY_ENV:-staging} expansion means "use $DEPLOY_ENV if it’s set and non-empty, otherwise use staging." Prefixing the command with DEPLOY_ENV=production sets that variable in the environment of just that one command, without exporting it into the rest of your interactive shell — a common pattern for one-off overrides.

How It Works, Step by Step

Walking through what happens when you run export API_KEY="abc123" followed by ./call-api.sh:

  • Bash parses the export builtin and stores API_KEY in its internal variable table, with an internal "export" flag set on it.
  • When you run ./call-api.sh, Bash calls fork(), which creates a near-identical copy of the shell process, including its memory and file descriptors.
  • The shell builds an environment array from every variable marked exported (this is a flat array of KEY=value C strings, terminated by a null pointer) and passes it to execve().
  • execve() replaces the child process’s program code with call-api.sh‘s interpreter (/usr/bin/env bash, per its shebang line), but the environment array survives the exec — the new program starts with API_KEY already present in its environment.
  • Inside call-api.sh, referencing $API_KEY just reads that value out of its own copy of the inherited environment; it has no way to write back into the parent shell’s variables.

Common Mistakes

1. Forgetting to export a variable a child process needs

API_KEY="sk-<YOUR_API_KEY>"
./call-api.sh

Because API_KEY was never exported, call-api.sh runs in a child process whose environment simply doesn’t contain it — the script sees an empty variable and likely fails with an authentication error. Fix it by exporting:

export API_KEY="sk-<YOUR_API_KEY>"
./call-api.sh

2. Overwriting PATH instead of extending it

export PATH="/usr/local/bin"

This replaces the entire search path with a single directory. Immediately afterward, ordinary commands like ls, cd-adjacent tools, and even sudo may stop resolving, because /bin, /usr/bin, and /sbin are no longer being searched. Always prepend or append to the existing value:

export PATH="/usr/local/bin:$PATH"

3. Leaving a variable’s value unquoted when it contains spaces

REPORT_NAME="monthly report.txt"
touch $REPORT_NAME

Because $REPORT_NAME is unquoted, Bash word-splits its value on whitespace before passing it to touch, so this creates two files, monthly and report.txt, instead of one. Quoting the expansion keeps it as a single argument:

REPORT_NAME="monthly report.txt"
touch "$REPORT_NAME"

Best Practices

  • Put personal environment variables in ~/.bashrc (sourced for interactive non-login shells) or ~/.profile (sourced for login shells); use /etc/environment or a script in /etc/profile.d/ for settings that should apply system-wide to every user.
  • Always quote variable expansions ("$VAR") in scripts and commands to avoid word-splitting and globbing surprises.
  • Use UPPER_CASE names for exported/environment variables by convention, and reserve lower_case for variables meant to stay local to a script or shell — it makes scope obvious at a glance.
  • Prefer ${VAR:-default} for a fallback value instead of assuming a variable is always set by whoever calls your script.
  • Never overwrite PATH outright; always prepend or append to its existing value.
  • Use env or printenv to debug exactly what a program sees, especially under cron or systemd, where the environment is often much smaller than your interactive shell’s.
  • Don’t put real secrets in shell history or version-controlled files; source them from a separate, gitignored file, or use a proper secrets manager.
  • Use unset to remove a variable you no longer want a child process to inherit, rather than setting it to an empty string.

Practice Exercises

  • Set a variable BACKUP_DIR to /var/backups/myapp and export it. Write a one-line script that echoes Backup directory: $BACKUP_DIR, and confirm with bash -c that a child shell can see the value.
  • Permanently add $HOME/bin to your PATH by editing ~/.bashrc, then create a small executable script inside that directory and run it by name alone, from any working directory, without ./ or a full path.
  • Write a script that reads a LOG_LEVEL environment variable using ${LOG_LEVEL:-info} as the default, and test it twice: once with no LOG_LEVEL set, and once running it as LOG_LEVEL=debug ./yourscript.sh.

Summary

  • An environment variable is a KEY=value pair attached to a process and automatically inherited by every child process it starts.
  • export promotes a shell variable into the environment so child processes can see it; without it, the variable stays private to the current shell.
  • PATH is an environment variable listing directories, in order, that the shell searches when you type a bare command name.
  • ${VAR:-default} supplies a fallback value when a variable is unset or empty, without changing the variable itself.
  • Persist variables you want every session to have in ~/.bashrc or ~/.profile; use /etc/environment for system-wide settings.
  • Always quote variable expansions ("$VAR") to avoid word-splitting bugs, and never overwrite PATH wholesale — extend it instead.