Skip to content

Variables and Environment

A shell variable lives only in the current shell; an environment variable is copied into every program the shell starts. Most "works in my terminal, fails in cron, sudo or systemd" problems are environment differences, so knowing where each variable comes from is a daily troubleshooting skill.

Track: Core · Interview weight: High


Must-Know Facts

Fact Value Verify with
Shell variable Visible only in the current shell set
Environment variable Exported; copied to child processes at exec env, printenv
Export export VAR=value or declare -x VAR declare -p VAR
One-command variable VAR=value command sets it for that command only LOG_LEVEL=debug env
Children cannot change parents A child's export never reaches the parent shell bash -c 'export X=1'; echo $X
Process environment Fixed at exec; later export does not change a running process tr '\0' '\n' < /proc/<pid>/environ
PATH Colon-separated directories searched in order; . is not included echo $PATH
Login shell files /etc/profile, /etc/profile.d/*.sh, then the first of ~/.bash_profile, ~/.bash_login, ~/.profile bash -l
Non-login interactive files ~/.bashrc (which sources /etc/bashrc on RHEL) bash -i
Scripts, cron, systemd Read no startup files bash -c env
System-wide static variables /etc/environment, read by pam_env at login cat /etc/environment
sudo Resets the environment and sets PATH from secure_path sudo printenv PATH
Read-only variable readonly VAR=value; cannot be changed or unset readonly -p

Shell Variables and Exported Variables

APP_ENV=staging
bash -c 'echo "child sees: [$APP_ENV]"'
export APP_ENV
bash -c 'echo "child sees: [$APP_ENV]"'
declare -p APP_ENV

Output:

child sees: []
child sees: [staging]
declare -x APP_ENV="staging"

declare -x marks the variable as exported. A variable set on the same line as a command reaches that command only:

unset APP_ENV
LOG_LEVEL=debug bash -c 'echo "one command: [$LOG_LEVEL]"'
echo "shell after: [$LOG_LEVEL]"

Output:

one command: [debug]
shell after: []

A subshell ( ... ) gets a copy of every variable, exported or not, and its changes stay inside it:

x=1; ( x=2; echo "subshell x=$x" ); echo "parent x=$x"

Output:

subshell x=2
parent x=1
Command Shows
set Every shell variable and function
env, printenv Exported variables only
declare -p VAR One variable with its attributes
export -p Every exported variable
unset VAR Removes a variable
env -i command Runs a command with an empty environment
env -i bash -c 'env'

Output:

PWD=/home/laborant
SHLVL=0
_=/usr/bin/env

Even with env -i, bash sets PWD, SHLVL and _ itself.


The Environment of a Running Process

Each process receives its environment in the execve call and keeps that copy. A later export in the parent changes the parent only:

export STAGE=blue
sleep 60 &
pid=$!
export STAGE=green
tr '\0' '\n' < /proc/$pid/environ | grep STAGE
echo "shell now: $STAGE"

Output:

STAGE=blue
shell now: green

Changing an environment file does not change running services

A service started before /etc/environment or a unit's Environment= was edited keeps the old values. Restart the service, then confirm with /proc/<pid>/environ (root is needed for other users' processes).


PATH

echo "$PATH" | tr ':' '\n'

Output:

/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin

Directories are searched left to right, and the first match wins. Prepend to prefer a directory; append to use it as a fallback:

export PATH="$HOME/.local/bin:$PATH"

Never put . or an empty entry in PATH

PATH=:/usr/bin or PATH=.:$PATH makes the shell run a file named ls from whatever directory is current, including world-writable ones such as /tmp.


Startup Files

strace shows which files bash opens for a login shell:

strace -f -e trace=openat -o /tmp/login.txt bash -l -i -c true 2>/dev/null
grep -v ENOENT /tmp/login.txt | grep -oE '"(/etc/[^"]*(profile|bashrc|environment)[^"]*|/home/laborant/\.[a-z_]+)"' | uniq

Output:

"/etc/profile"
"/etc/profile.d/"
"/etc/profile.d/70-systemd-shell-extra.sh"
"/etc/profile.d/bash_completion.sh"
# ... (trimmed)
"/etc/profile.d/which2.sh"
"/etc/profile.d/sh.local"
"/etc/bashrc"
"/home/laborant/.bash_profile"
"/home/laborant/.bashrc"
"/etc/bashrc"

Output:

"/etc/profile"
"/etc/bash.bashrc"
"/etc/profile.d/"
"/etc/profile.d/01-locale-fix.sh"
"/etc/profile.d/bash_completion.sh"
"/home/laborant/.profile"
"/home/laborant/.bashrc"
"/home/laborant/.bash_history"

The same trace for bash -i -c true (interactive, non-login) opens only ~/.bashrc and the files it sources, and bash -c true opens none.

Shell type RHEL / Rocky Ubuntu / Debian
Login (SSH, su -, bash -l) /etc/profile (sources profile.d and /etc/bashrc), ~/.bash_profile (sources ~/.bashrc) /etc/profile (sources /etc/bash.bashrc and profile.d), ~/.profile (sources ~/.bashrc)
Interactive non-login (new terminal tab) ~/.bashrc (sources /etc/bashrc, which sources profile.d) /etc/bash.bashrc, ~/.bashrc
Non-interactive (scripts, cron, systemd) None ($BASH_ENV if set) None ($BASH_ENV if set)
Change Where to put it
Aliases, functions, prompt for one user ~/.bashrc
Environment for one user's logins ~/.bash_profile (RHEL) or ~/.profile (Ubuntu)
Environment for every user's shells A new file in /etc/profile.d/
Static KEY=value for every session, any shell /etc/environment
A service Environment= or EnvironmentFile= in the unit
A cron job Variables at the top of the crontab, or in the script

Editing a startup file does not affect the current shell

source ~/.bashrc (or . ~/.bashrc) re-reads it in the current shell. Running bash ~/.bashrc would load it into a child that exits immediately.


Environments Without Startup Files

grep -E '^(SHELL|PATH|MAILTO)' /etc/crontab
sudo printenv PATH
sudo grep -E '^Defaults\s+(env_reset|secure_path)' /etc/sudoers

Output:

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
/sbin:/bin:/usr/sbin:/usr/bin
Defaults    env_reset
Defaults    secure_path = /sbin:/bin:/usr/sbin:/usr/bin
cat /etc/environment
export APP_ENV=staging
sudo bash -c 'echo "sudo sees: [$APP_ENV]"'
sudo -E bash -c 'echo "sudo -E sees: [$APP_ENV]"'
sudo printenv PATH

Output:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
sudo sees: []
sudo -E sees: [staging]
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin

A systemd service gets only what the unit defines, plus systemd's default PATH:

sudo systemd-run --unit=envdemo --wait -q -p Environment=STAGE=svc bash -c 'echo "service sees STAGE=[$STAGE] PATH=$PATH"'
sudo journalctl -u envdemo -o cat --no-pager | grep sees

Output:

service sees STAGE=[svc] PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin

sudo -E keeps the caller's variables only when the sudoers policy allows it; sudo VAR=value command passes one variable if setenv or the env_keep list permits it.


Read-Only Variables

Inside a script:

readonly DB_PORT=5432
DB_PORT=5433
echo "rc=$?"

Output:

/tmp/var.sh: line 19: DB_PORT: readonly variable
rc=1

readonly cannot be undone in the running shell. TMOUT is often made read-only in /etc/profile.d/ to force idle logouts that users cannot disable.


Common Errors

sudo: mytool: command not found

Cause: sudo replaces PATH with secure_path, which does not contain /usr/local/bin on RHEL or the user's ~/.local/bin.

Fix: call the tool by full path (sudo "$(command -v mytool)"), or add the directory to secure_path with visudo.


Interview Checkpoints

L1: What is the difference between a shell variable and an environment variable?

Say first: a shell variable stays in the current shell; an exported variable is copied into the environment of every program the shell starts.

Proof: X=1; bash -c 'echo $X' prints nothing; after export X, it prints 1.

Follow-up: Can a child process change a variable in its parent?

L1: What is the difference between .bashrc and .bash_profile?

Say first: login shells read ~/.bash_profile (or ~/.profile on Ubuntu); interactive non-login shells read ~/.bashrc; the profile usually sources .bashrc so both get the same aliases.

Proof: strace -e trace=openat bash -l -i -c true lists the files opened.

Follow-up: Which of them does a cron job read? (Neither.)

L1: Why is the current directory not in PATH?

Say first: a file named like a system command in a writable directory would run instead of the real one.

Proof: echo $PATH has no .; local files run as ./name.

Follow-up: What does an empty entry (::) in PATH mean?

L2: Add ~/bin to PATH permanently for one user.

Say first: prepend it in a startup file that every interactive shell reads.

Proof:

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
command -v mytool

Follow-up: How would you set it for every user? (/etc/profile.d/custom-path.sh.)

L2: Show the environment of a running process, not of your shell.

Say first: read /proc/<pid>/environ, which is NUL-separated.

Proof: sudo tr '\0' '\n' < /proc/$(pgrep -o nginx)/environ

Follow-up: Why can this differ from the service's current unit file?

L2: Run one command with a variable set, without changing the shell.

Say first: put the assignment in front of the command.

Proof: LOG_LEVEL=debug ./app

Follow-up: How do you run a command with no inherited environment? (env -i.)

L3: A script works in an SSH session but fails in cron with command not found.

Say first: compare PATH in both; cron reads no startup files.

Proof: a job line * * * * * env > /tmp/cron-env shows PATH=/usr/bin:/bin or the value in /etc/crontab; the tool lives in /usr/local/bin or ~/.local/bin.

Follow-up: Name two fixes. (Full paths, or PATH= at the top of the crontab.)

L3: An application reads a new value from /etc/environment in a shell, but the service still uses the old one.

Say first: services do not read /etc/environment through a login, and a running process keeps its original environment.

Proof: sudo tr '\0' '\n' < /proc/<pid>/environ shows the old value; systemctl show <unit> -p Environment shows what the unit sets.

Follow-up: Where should the value go? (Environment= or EnvironmentFile=, then restart.)

L3: sudo mytool says command not found, but mytool works without sudo.

Say first: sudo swaps PATH for secure_path.

Proof: sudo printenv PATH lacks the directory that command -v mytool reports.

Follow-up: Why is secure_path a security control?

L4: How does a program receive its environment, and why can't a child change the parent's?

Say first: the kernel copies the argument and environment strings passed to execve onto the new process's stack; each process owns its copy, and no system call writes into another process's environment.

Proof: strace -e trace=execve -v bash -c 'env >/dev/null' shows the environment array; /proc/<pid>/environ stays fixed after export in the parent.

Don't say: "Environment variables are global to the system."


Captured on Rocky Linux 10.2 and Ubuntu 24.04.4 LTS (iximiuz Labs microVMs, kernel 6.1.167), 2026-09.