Environment Variables and PATH

Table of Contents

  1. Variables in the Shell
  2. Environment Variables
  3. export: Passing Variables to Programs
  4. How Apps Use Environment Variables
  5. PATH: Where the Shell Looks for Commands
  6. Why "Command Not Found" Happens
  7. Making Changes Permanent: .zshrc and .bashrc
  8. Aliases: Your Own Shortcuts
  9. 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 = Phnom doesn't create a variable — the shell thinks city is a command and = and Phnom are 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
VariableHolds
HOMEYour home folder — this is what ~ means
USERYour username
SHELLWhich shell you use
PWDThe current directory (what pwd prints)
PATHWhere the shell looks for commands (section 5)
EDITORWhich 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:

  1. 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.
  2. The installer didn't add it to PATH at all. Find out where the program was installed, then add that folder to PATH yourself.
  3. A typo — nodejs vs. node, python vs. python3.
  4. 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:

ShellStartup 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

  1. Create a variable name containing your full name (with a space) and print Hello, <your name>! using echo. Try it with both double and single quotes.
  2. Print your HOME, USER, and SHELL. Then find HOME in the output of env using grep.
  3. Repeat the bash -c experiment from section 3: show that a variable isn't visible to the child until you export it.
  4. Print your PATH with each folder on its own line: echo $PATH | tr ':' '\n'. (tr translates one character into another — here, colons into new lines.) Which folder contains git? Check with which git.
  5. Start a child bash, set PATH=/nowhere, try ls, then exit. Confirm ls works again.
  6. Add alias ll="ls -lah" and export EDITOR="nano" to the end of your startup file, source it, 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!