Environment Variables
Reviewed & published by Brayan K
By the end of this lesson you'll understand how the shell stores values, the difference between a shell variable and an environment variable, how PATH lets you run commands by name, and how to make your settings persist across every terminal session.
Part of the free Command Line course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Tell shell variables from environment variables and use export
- Read values with $VAR and ${VAR}, and inspect with env / printenv
- Understand PATH, HOME, and how the shell finds commands
- Read the exit status of the last command from $?
- Remove variables with unset
- Persist variables in .bashrc, .bash_profile, and .zshrc
1. Shell Variables vs Environment Variables
A plain assignment name=value creates a shell variable that only the current shell can see. To let a program you launch — a child process — inherit it, you must export it into the environment. That single distinction explains most "why can't my program see my variable?" confusion.
# A plain assignment makes a SHELL variable. No spaces around =.
greeting="hello"
# echo can read it in THIS shell...
echo "$greeting"
# ...but a child process (a new bash here) does NOT inherit it,
# because it was never exported. The child prints a blank line.
bash -c 'echo "child sees: [$greeting]"'
# 'export' promotes the variable INTO the environment, so any
# child process the shell launches will inherit a copy.
export greeting
bash -c 'echo "child sees: [$greeting]"'
# You can assign and export in one step:
export project="learn-cli"
bash -c 'echo "project is $project"'2. Reading and Inspecting Variables
Expand a value with $VAR or ${VAR} — use the braces when the name touches other text. To list the whole environment use env (or printenv with no argument); to read one variable use printenv NAME. Note that printenv only sees exported variables, while echo $VAR expands any shell variable.
# Three ways to look at the environment.
# env (and printenv with no args) lists EVERY environment variable.
# There are usually dozens, so here we just count them:
env | wc -l
echo "--- read one specific variable ---"
# printenv NAME prints just that one variable's value.
printenv HOME
echo "--- $VAR vs ${VAR} ---"
# $VAR and ${VAR} do the same thing: expand the value.
# Use the braces when the name touches other text, so the shell
# knows where the name ends.
user="ada"
echo "Home is $HOME"
echo "Backup dir: ${user}_backup" # braces needed: user_backup would be one name3. PATH: How the Shell Finds Commands
PATH is a colon-separated list of directories. When you type ls, the shell walks PATH left to right and runs the first ls it finds. To add your own folder, prepend it and keep the old value on the end with :$PATH — overwriting PATH entirely is how people accidentally make every command "not found".
# PATH is a COLON-separated list of directories. When you type a
# command, the shell searches these folders, left to right, for it.
echo "$PATH"
echo "--- which directory did a command come from? ---"
# 'which' (or 'type') shows the first match found on the PATH.
which ls
echo "--- add a directory to the FRONT of PATH ---"
# Put your own bin first so your scripts win. Keep the old PATH on
# the end with :$PATH so you don't lose the system directories.
export PATH="$HOME/bin:$PATH"
echo "$PATH"4. The Exit Status: $?
The special variable $? holds the exit status of the command that just finished. By convention 0 means success and any non-zero value means a failure. Scripts lean on this constantly — it's how if decides whether the previous command worked.
# Every command sets $? to its exit status the moment it finishes.
# 0 means success; any non-zero value means some kind of failure.
ls /etc/hostname > /dev/null # this file exists -> success
echo "after success: $?" # 0
ls /no/such/file 2> /dev/null # this fails -> non-zero
echo "after failure: $?" # 2 (a typical 'No such file' code)
# A common pattern: act on whether the previous command worked.
if grep -q "root" /etc/passwd; then
echo "found root, exit was $?"
fi5. Removing Variables with unset
When you no longer need a variable — especially a secret like an API token — remove it with unset. After unsetting, expanding the name gives an empty string because the variable simply no longer exists.
# unset removes a variable completely.
export token="abc123"
echo "before: [$token]"
unset token
echo "after: [$token]" # empty — the variable is gone
# Unsetting is handy for secrets you no longer want in the
# environment, and for clearing a variable a child might inherit.6. Persisting Variables Across Sessions
Anything you set in a shell vanishes when that shell closes. To load a variable every session, add the export line to a startup file. The right file depends on your shell and session type: ~/.bashrc for interactive non-login bash, ~/.bash_profile for login bash, and ~/.zshrc for zsh (the default on modern macOS). Use source to apply changes without opening a new terminal.
# Variables set in a shell vanish when that shell closes. To make
# one load EVERY session, add the export line to a startup file.
#
# ~/.bashrc bash: interactive NON-login shells (new terminal tabs on Linux)
# ~/.bash_profile bash: LOGIN shells (first login, many macOS terminals)
# ~/.zshrc zsh: interactive shells (default shell on modern macOS)
# Append a line to ~/.bashrc (>> adds, > would overwrite — be careful):
echo 'export EDITOR=nano' >> ~/.bashrc
# Apply it to the CURRENT shell without opening a new terminal:
source ~/.bashrc # 'source' (or '.') runs the file in this shell
echo "EDITOR is now $EDITOR"🎯 Your Turn: export to a child process
Fill in the one blank marked ___ using the # 👉 hint so the child bash can see the variable, then run it and check the Output panel matches.
# 🎯 YOUR TURN — make a child process see your variable.
api_url="https://example.com"
# 1) Promote api_url into the environment so children inherit it.
___ api_url # 👉 replace ___ with export
# 2) The child bash should now print the value.
bash -c 'echo "url: $api_url"'
# ✅ Expected output:
# url: https://example.com🎯 Your Turn: extend PATH safely
Add a new folder to the front of PATH while keeping everything that was already there. Fill in the blank, then run it.
# 🎯 YOUR TURN — add a folder to PATH without losing the old one.
# 1) Prepend ~/tools but KEEP the existing PATH on the end.
export PATH="$HOME/tools:___" # 👉 replace ___ with $PATH
echo "$PATH"
# ✅ Expected output (your existing PATH will follow the new folder):
# /home/ada/tools:/usr/local/bin:/usr/bin:/binPro Tips
- 💡 Convention: environment variable names are written in UPPER_CASE (like PATH, HOME), while temporary script variables are usually lower-case. This makes exported settings easy to spot.
- 💡 Set a variable for just one command by prefixing it: DEBUG=1 ./run.sh sets DEBUG only for that single run, leaving your shell untouched.
- 💡 source ~/.bashrc (or the shortcut . ~/.bashrc) reloads your startup file into the current shell — no need to close and reopen the terminal.
- 💡 Never echo secrets into the environment in shared logs. Use unset to clear tokens when you're done, and prefer per-command assignment for sensitive values.
Common Errors (and the fix)
- My program can't see my variable — you set it but never exported it. Only exported (environment) variables are inherited by child processes.
- Every command is suddenly "not found" — you overwrote PATH instead of appending. Fix the current shell with export PATH="/usr/local/bin:/usr/bin:/bin:$PATH" and use :$PATH next time.
- "command not found" right after name = value — you put spaces around =. Assignment needs no spaces: name=value.
- $file_backup is empty — the shell looked for a variable literally named file_backup. Use braces: ${file}_backup.
- My export vanished after closing the terminal — it was only in that shell. Add the line to ~/.bashrc or ~/.zshrc to load it every session.
📋 Quick Reference
| Task | Command | Notes |
|---|---|---|
| Set shell variable | name="value" | No spaces around = |
| Export to environment | export name | Children inherit it |
| Read value | $name / ${name} | Braces near other text |
| List all | env | printenv also works |
| Read one | printenv NAME | Exported vars only |
| Command search path | $PATH | Colon-separated dirs |
| Extend PATH | export PATH="dir:$PATH" | Keep old value |
| Last exit status | $? | 0 = success |
| Remove variable | unset name | Deletes it |
| Persist a variable | echo 'export X=1' >> ~/.bashrc | Loads each session |
Mini-Challenge: an environment report
No blanks this time — just a brief and an outline. Combine export, $?, PATH inspection, and unset into a few commands that demonstrate everything from this lesson, and check your output against the hints in the comments.
# 🎯 MINI-CHALLENGE: a tiny environment report
# Write a few commands (or a small script) that:
#
# 1. Sets a SHELL variable app="demo" and proves a child bash
# does NOT see it (export it, then prove the child DOES).
# 2. Prints your HOME and the number of directories on your PATH
# (hint: turn the colons into newlines, then wc -l).
# 3. Runs a command you know will fail and prints its $? exit code.
# 4. unset the app variable at the end.
#
# Hint for #2: echo "$PATH" | tr ':' '\n' | wc -l
# your commands here🎉 Lesson Complete!
- ✅ A plain assignment is a shell variable; export makes it an environment variable children inherit
- ✅ Read values with $VAR / ${VAR}; list with env; read one with printenv NAME
- ✅ PATH is a colon-separated list the shell searches for commands — extend it with :$PATH
- ✅ $? holds the last exit status; unset removes a variable
- ✅ Persist variables by adding export lines to ~/.bashrc, ~/.bash_profile, or ~/.zshrc
Practice quiz
How do you read the value of an environment variable named HOME?
- $HOME
- &HOME
- @HOME
- *HOME
Answer: $HOME. Prefix the name with a dollar sign: $HOME (or ${HOME}).
Which command marks a shell variable so child processes can see it?
- set
- local
- export
- env
Answer: export. export promotes a shell variable into the environment so child processes inherit it.
What does the PATH variable hold?
- The current user name
- A colon-separated list of directories searched for commands
- The shell prompt string
- Your home directory
Answer: A colon-separated list of directories searched for commands. PATH is a colon-separated list of directories the shell searches to find executables.
Which command prints ALL current environment variables?
- echo
- printf
- list
- env
Answer: env. env (or printenv with no argument) prints every variable in the environment.
What does $? contain after a command finishes?
- The exit status of the last command
- The process ID
- The current directory
- The shell name
Answer: The exit status of the last command. $? holds the exit status of the most recent command: 0 means success, non-zero means failure.
What is the difference between a shell variable and an environment variable?
- Shell variables are numbers, environment variables are text
- A shell variable is local to the current shell until you export it into the environment
- Environment variables cannot be changed
- There is none
Answer: A shell variable is local to the current shell until you export it into the environment. A plain assignment makes a shell variable; export turns it into an environment variable inherited by child processes.
Which command removes a variable entirely?
- delete VAR
- rm VAR
- clear VAR
- unset VAR
Answer: unset VAR. unset VAR removes the variable from the shell and environment.
Why use printenv HOME instead of echo $HOME in scripts?
- echo cannot read variables
- There is no difference at all
- printenv only prints variables that are actually exported in the environment
- printenv is faster
Answer: printenv only prints variables that are actually exported in the environment. printenv reads the environment directly, so it shows nothing for an unexported shell variable, while echo $HOME expands any shell variable.
Which file is read for each new interactive non-login bash shell, such as a new terminal tab on Linux?
- ~/.bash_profile
- ~/.bashrc
- /etc/shadow
- ~/.ssh/config
Answer: ~/.bashrc. ~/.bashrc runs for interactive non-login shells; .bash_profile runs for login shells.
How do you make a variable persist across all future terminal sessions?
- Add the export line to a startup file like ~/.bashrc or ~/.zshrc
- Run set -x
- Save it with the save command
- Type export once in the current shell
Answer: Add the export line to a startup file like ~/.bashrc or ~/.zshrc. Variables set in the current shell vanish when it closes; add the export line to a startup file to load it every session.
Continue this course
- Previous: Permissions and Users
- Next: Globbing and Wildcards — Match files with *, ?, [...], brace expansion and the globstar **
- Quick reference: Command Line cheat sheet › Finding Things
Frequently asked questions
What is the difference between a shell variable and an environment variable?
A plain assignment like name="Ada" creates a SHELL variable that only the current shell can see. Running export name turns it into an ENVIRONMENT variable, which means every child process the shell launches inherits a copy. Programs you run can only read environment variables, not un-exported shell variables.
Why do I write $HOME but export HOME (no dollar sign)?
The dollar sign means 'give me the value of'. You use $HOME when you want the value, e.g. echo $HOME. But export, unset, and assignment all act on the NAME of the variable, so you write them without the dollar sign: export HOME, unset HOME, HOME=/tmp.
What exactly is PATH and why do commands break when I edit it wrong?
PATH is a colon-separated list of directories the shell searches to find the program for a command you type. If you overwrite it with export PATH="/my/dir" you wipe out /usr/bin and friends, so suddenly ls and grep are 'not found'. Always append the old value: export PATH="/my/dir:$PATH".
Which startup file should I put my exports in: .bashrc, .bash_profile, or .zshrc?
It depends on your shell and session type. ~/.bashrc runs for interactive non-login bash shells (new terminal tabs on Linux). ~/.bash_profile runs for login shells (first login, and many macOS terminal windows). If your shell is zsh (the default on modern macOS), use ~/.zshrc. When in doubt, run echo $0 to see your shell.
Why did my exported variable disappear after I closed the terminal?
export only affects the current shell and its children, and it all goes away when that shell exits. To make a variable load every time, add the export line to a startup file like ~/.bashrc or ~/.zshrc. Then either open a new terminal or run source ~/.bashrc to load it into the current one.