Environment variables & PATH
Learn what environment variables are, how PATH controls where the shell finds commands, and why 'command not found' happens — and how to fix it.
What you'll learn
- What environment variables are and how to read and set them
- How PATH controls command resolution, left-to-right through directories
- How to make changes persist across sessions using shell startup files
Before you start
The last lesson left two invisible mechanisms unexplained: how the shell knows which file git is, and where a script reads its secret API key from. Both have the same answer — the environment, a table of named variables every process inherits, with PATH (the command-finder) as its star. This lesson opens that table, and “command not found” turns from a mystery into a one-line fix.
What is an environment variable?
Every process on your computer runs inside an environment — a small table of key/value string pairs that the operating system hands to the process when it starts. These are called environment variables.
Programs read this table to configure themselves without needing command-line flags every time. Your text editor reads $EDITOR. Git reads $HOME. Servers read $PORT and $DATABASE_URL. The shell itself reads dozens of them.
Print your own home directory:
echo $HOME
/Users/alice
The $ prefix tells the shell to expand the variable — substitute its value before running the command.
To see the entire environment, use either of these:
env
printenv
Both dump every variable the current shell process inherited, one per line, in NAME=value format. The list is usually long — scroll up to browse it.
To look up a single variable:
printenv HOME
/Users/alice
Setting an environment variable
Use export to create or update a variable and make it visible to child processes (programs you launch from the shell):
export GREETING="hello world"
echo $GREETING
hello world
Three things to memorize:
- No spaces around
=.export NAME=valueworks.export NAME = valueis a syntax error — the shell interprets the spaces as argument boundaries. - The value is just a string. Numbers, paths, JSON — all strings.
exportis required if you want child processes to see the variable. A bareNAME=valueassignment stays local to the current shell.
What is PATH?
$PATH is an environment variable with one job: it is a colon-separated list of directory paths that the shell searches, in order, when you type a command name.
echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
When you type python3, the shell does not search the whole disk. It walks through each directory in $PATH, left to right, and checks whether a file named python3 exists and is executable. The first match wins.
“Command not found” means the shell walked every directory in your $PATH and found nothing. The tool is installed somewhere — just not in any of those directories.
Inspecting command resolution
Two commands let you see exactly where a command is found:
which python3
/usr/local/bin/python3
type python3
python3 is /usr/local/bin/python3
type is slightly more informative — it also tells you when a name is a shell builtin, a function, or an alias rather than an executable file.
Adding a directory to PATH
Say you have scripts in ~/bin and you want to run them by name. Add that directory to your PATH:
export PATH="$HOME/bin:$PATH"
Break this down:
$HOME/bin— the new directory you are adding:— the colon separator$PATH— the existing value, preserved
The new directory goes on the left, so it takes precedence over anything with the same name in later directories.
Making changes persist: startup files
Environment variables you export in a terminal session disappear when that session closes. To make them permanent, add the export line to your shell’s startup file (also called an rc file — “rc” stands for “run commands”).
| Shell | File to edit |
|---|---|
| zsh (macOS default) | ~/.zshrc |
| bash (Linux default, older macOS) | ~/.bashrc and/or ~/.bash_profile |
Open ~/.zshrc in any editor and add your export at the bottom:
export PATH="$HOME/bin:$PATH"
export EDITOR="vim"
Save the file. The change takes effect in every new shell session. To pick it up in the current session without opening a new tab, source the file:
source ~/.zshrc
Sourcing means “read this file and run every line as if I had typed it myself.” The word source can also be written as a single dot: . ~/.zshrc.
.env files for project secrets
For project-specific settings — API keys, database URLs, feature flags — teams conventionally use a .env file at the project root:
DATABASE_URL=postgres://localhost/myapp
API_KEY=abc123
DEBUG=true
This file is never committed to version control (add .env to your .gitignore). To load it into your current shell:
source .env
Many frameworks (Python python-dotenv, Node dotenv) load .env automatically when your app starts, so you often do not need to source it manually.
Quick reference
echo $HOME # print one variable
env # print all variables
printenv VARNAME # print one variable (no $ prefix needed)
export NAME=value # set a variable (no spaces around =)
export PATH="~/bin:$PATH" # prepend a directory to PATH
which cmd # show the full path of a command
type cmd # show how the shell resolves a name
source ~/.zshrc # reload startup file in current session
In one breath
Every process inherits an environment — a table of NAME=value string pairs (see it with env/printenv, read one with $NAME). Create them with export NAME=value (no spaces around =; export is what makes child processes see it). The star variable is PATH, a colon-separated list of directories the shell walks left to right to resolve a command — first match wins, and “command not found” just means none of them held it (which/type show where one resolves). Add a directory with export PATH="$HOME/bin:$PATH" — and never drop the trailing :$PATH, or you erase every standard command at once. These vanish when the session ends, so put them in a startup file (~/.zshrc / ~/.bashrc) and source it to apply now; keep project secrets in a gitignored .env.
Practice
Before the quiz, reason through a real install. You just built a tool at ~/tools/bin/deploy, but typing deploy gives command not found. What single export line fixes it for this session, where do you put that line so it survives a new terminal, and what one command applies it right now without opening a new tab? Then name the subtle disaster of writing export PATH="$HOME/tools/bin" instead.
Quick check
A question to carry forward
That closes the Power Tools — and notice the quiet assumption running under every lesson so far: that the machine you are working on is the one in front of you. Your files, your processes, your environment, all local. But that is almost never where the real work lives. The dataset sits on a GPU box down the hall; the bug you are chasing is on a production server in another country; the data you need is behind a web API with no website to click. The terminal’s true superpower is reach — the ability to open a shell on a machine you will never physically touch, copy files across the network to it, and talk to a web service with no browser at all. Stepping off your own machine and onto everyone else’s is where the final chapter, Beyond the Basics, begins — starting with ssh and curl.