Environment Variables and PATH
Table of Contents
- Variables in the Shell
- Environment Variables
export: Passing Variables to Programs- How Apps Use Environment Variables
- PATH: Where the Shell Looks for Commands
- Why "Command Not Found" Happens
- Making Changes Permanent:
.zshrcand.bashrc - Aliases: Your Own Shortcuts
- Try It Yourself
1. Variables in the Shell
Like a programming language, the shell can store values under a name — a
variable. Create one with name=value:
city=Phnom Penh
zsh: command not found: Penh
Two rules trip everyone up at first:
- No spaces around
=.city = Phnomdoesn't create a variable — the shell thinkscityis a command and=andPhnomare its arguments. - Quote values that contain spaces. Otherwise the shell stops at the first space, as you saw above.
So:
city="Phnom Penh"
To use a variable, put a $ in front of its name. The shell
replaces $city with its value before running the command:
echo "I live in $city"
I live in Phnom Penh
Use double quotes around text that contains variables. Single quotes
(chapter 4) are strict: the shell leaves everything inside them exactly
as written, $ included:
echo 'I live in $city'
I live in $city
Variables only live in the current shell session. Close the terminal
and city is gone.
2. Environment Variables
Some variables are set up for you before you've typed anything. They're called environment variables, and they describe the environment your programs run in. By convention, their names are in CAPITALS:
echo $HOME
echo $USER
echo $SHELL
/Users/dara
dara
/bin/zsh
| Variable | Holds |
|---|---|
HOME | Your home folder — this is what ~ means |
USER | Your username |
SHELL | Which shell you use |
PWD | The current directory (what pwd prints) |
PATH | Where the shell looks for commands (section 5) |
EDITOR | Which editor programs like Git open for you |
To see all of them:
env
That's a long list — pipe it through sort or less, or search it with
env | grep HOME.
3. export: Passing Variables to Programs
There's an important difference between a plain shell variable and an environment variable. Remember from chapter 8 that every command is a child process of your shell. A plain variable stays inside the shell. An environment variable is copied into every child process the shell starts.
You can see this using bash -c, which starts a new child shell to run
a single command:
GREETING="Hello"
bash -c 'echo "The child sees: $GREETING"'
The child sees:
The child got nothing. Now export it, which turns it into an
environment variable:
export GREETING
bash -c 'echo "The child sees: $GREETING"'
The child sees: Hello
You can create and export in one step:
export GREETING="Hello"
The single quotes around the bash -c command matter here: they stop
your shell from replacing $GREETING itself, so the child shell is
the one that looks it up.
There's also a shortcut for setting a variable for just one command, without keeping it afterwards — put it in front:
GREETING="Hi" bash -c 'echo $GREETING'
You'll see this pattern in real projects, like PORT=4000 npm run dev
to start a dev server on a different port for one run.
4. How Apps Use Environment Variables
Environment variables are how you configure an app without changing its code. The same code can run on your laptop, a test server, and in production, reading different settings from its environment each time:
// In a Node.js app
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
They're also where secrets live — database passwords, API keys — so they never get written into the code and committed to Git.
In real projects you'll usually find them in a .env file in the
project folder:
PORT=3000
DATABASE_URL=postgres://localhost:5432/myapp
API_KEY=replace-me
Your framework or a library reads that file and loads each line into
process.env when the app starts. Because it contains secrets, .env
is listed in .gitignore so Git never uploads it. You'll set these up
for real in the NestJS and Docker series.
5. PATH: Where the Shell Looks for Commands
When you type git, how does the shell know which file to run? It looks
in the folders listed in the PATH variable:
echo $PATH
/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
PATH is a list of folders separated by colons. When you type a
command, the shell checks each folder in order, from left to right,
and runs the first matching program it finds. If it gets to the end
without a match, you get command not found.
That's exactly what which (chapter 7) shows you — the result of that
search. And which -a shows every match, in the order PATH checks them.
If you have two versions of Node.js installed, the one in the folder
listed earliest in PATH wins.
You can see what happens without PATH by breaking it — safely, inside a child shell that you'll throw away:
bash
PATH=/nowhere
ls
bash: ls: command not found
ls is still there in /bin — the shell just doesn't know where to
look anymore. Type exit to leave the child shell, and your original
shell, with its PATH untouched, is back. This is also a useful lesson:
variable changes only affect the shell you make them in.
Adding a folder to PATH
To add a folder, put it in front of the existing list, joined with a colon:
export PATH="$HOME/bin:$PATH"
Read it carefully: the new PATH is "$HOME/bin, then a colon, then
whatever PATH was before". Putting it first means your programs win
over system ones with the same name. Forgetting the :$PATH part
replaces the list instead of adding to it, and almost every command
stops working — if that happens, just close the terminal and open a new
one.
You'll put a folder of your own on PATH in chapter 11.
6. Why "Command Not Found" Happens
Now the most confusing error for beginners makes sense. You install a tool, the installer says it worked, you type its name, and:
zsh: command not found: node
The usual causes, in order of likelihood:
- The terminal was open before you installed it. Installers add the tool's folder to PATH in your shell's startup file (section 7), but your open terminal read that file when it started, before the change. Open a new terminal and try again. This fixes it most of the time.
- The installer didn't add it to PATH at all. Find out where the program was installed, then add that folder to PATH yourself.
- A typo —
nodejsvs.node,pythonvs.python3. - It's installed on the other side. On Windows, a program installed on Windows isn't automatically available inside WSL, and the other way round. Install Linux tools inside WSL.
And when the wrong version runs, use which -a to see which copy is
first in PATH.
7. Making Changes Permanent: .zshrc and .bashrc
Everything you've done so far disappears when you close the terminal. To keep a setting, put it in your shell's startup file — a hidden file in your home folder that the shell runs every time a new terminal opens:
| Shell | Startup file |
|---|---|
| zsh (macOS default) | ~/.zshrc |
| Bash (Ubuntu, WSL, Git Bash) | ~/.bashrc |
Not sure which one? Run echo $SHELL.
Open it with nano (chapter 5). If it doesn't exist yet, nano creates
it:
nano ~/.zshrc # or: nano ~/.bashrc
Go to the very end of the file and add a line:
export EDITOR="nano"
Save with Ctrl + O, Enter, and exit with Ctrl + X. The change
isn't active yet in this terminal — the file is only read when a shell
starts. Either open a new terminal, or tell the current shell to re-read
it with source:
source ~/.zshrc # or: source ~/.bashrc
echo $EDITOR
This is exactly what installers like nvm, Homebrew, and
pyenv do: they add a few lines to this file. If you ever wonder what
your shell is doing at startup, cat ~/.zshrc and read it.
Be careful editing it: a mistake in this file runs every time you open a terminal. If a new terminal shows errors right after an edit, open the file again, look at the lines you just added, and fix them. Adding to the end of the file and changing one thing at a time makes mistakes easy to find.
8. Aliases: Your Own Shortcuts
An alias is a short name for a longer command. You met one in
chapter 7: type ls showed that ls may already be an alias.
alias ll="ls -lah"
ll
Now ll runs ls -lah. A few popular ones:
alias ll="ls -lah"
alias ..="cd .."
alias gs="git status"
Like variables, an alias typed in the terminal disappears when you close
it. To keep it, add the alias line to your ~/.zshrc or ~/.bashrc
and source it.
Run alias on its own to list all your aliases, and unalias ll to
remove one. Keep aliases for things you type many times a day — and
remember that they only exist on your machine, so don't use them
in scripts or instructions for other people.
9. Try It Yourself
- Create a variable
namecontaining your full name (with a space) and printHello, <your name>!usingecho. Try it with both double and single quotes. - Print your
HOME,USER, andSHELL. Then findHOMEin the output ofenvusinggrep. - Repeat the
bash -cexperiment from section 3: show that a variable isn't visible to the child until youexportit. - Print your
PATHwith each folder on its own line:echo $PATH | tr ':' '\n'. (trtranslates one character into another — here, colons into new lines.) Which folder containsgit? Check withwhich git. - Start a child
bash, setPATH=/nowhere, tryls, thenexit. Confirmlsworks again. - Add
alias ll="ls -lah"andexport EDITOR="nano"to the end of your startup file,sourceit, and test both. Open a brand new terminal and check they're still there.
You now understand why commands are found — or not — and how to make
your shell your own. Next, let's look at who is allowed to do what:
permissions and sudo.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!