MA-CLI1 · Module 1.10
Environment and PATH
Topics: Environment · PATH · export
Overview
Module 1.10 — Environment and PATH
Every shell session carries a small set of named settings called environment variables. One of them, PATH, is the reason typing ls works from any folder. In this module you read those variables, learn when a value stays inside the shell and when it is handed to the programs you start, and see how the shell decides which program to run.
By the end you should be able to:
- print variables with
echo $HOME,$USER, and$PATH; - explain
exportversus a plain assignment; - read
PATHleft to right and predict which copy of a command wins; - use
whichandcommand -vto find where a command lives; - set a value for just one command with
VAR=value command.
Dotfiles such as ~/.zshrc and ~/.bashrc get a brief, gentle mention only.
Scroll Core ideas → worked examples → Quiz. Plan on roughly 45–55 minutes.
Concepts
Core ideas — the settings your shell carries with it
So far Sam has typed commands such as ls, cp, and chmod without ever asking a basic question: how does the shell know where those programs are? There is no ls file in ~/cli-practice/tidewatch/, yet ls works there. It also works in /tmp, in your home folder, and anywhere else you cd to.
The answer is that every shell session carries a small collection of named settings along with it. These settings are called the environment. Programs you launch can read them, and the shell itself uses a few of them to make everyday life work. This module opens that collection, shows you how to read it, how to add to it, and how to use it to find out exactly which program the shell would run.
None of this changes your files. It is mostly about looking and about understanding a mechanism that otherwise feels like magic.
Environment variables — named values
A variable is a name attached to a piece of text. An environment variable is a variable that lives in the shell's environment, where other programs can see it too. By convention, environment variable names are written in UPPERCASE with underscores, such as HOME or PATH. Nothing forces the capitals, but nearly everyone follows the habit, and it helps you tell them apart from ordinary words.
To read a variable, put a dollar sign in front of its name. The shell replaces $NAME with the stored text before running the command:
echo $HOME
echo $USER
/Users/sam
sam
echo itself knows nothing about variables. It simply prints whatever arguments it receives. The shell did the substitution first, so echo was handed /Users/sam and sam and printed them back. Keep that two-step picture in mind: the shell expands, then the command runs.
Three variables are worth meeting right away:
| Variable | What it holds | Typical value |
|----------|---------------|---------------|
| HOME | your home folder; the same place ~ points to | /Users/sam |
| USER | your login name | sam |
| PATH | a list of folders where the shell looks for programs | several folders separated by colons |
You can see all of the variables currently in the environment by running env (or printenv). The output is long and a little noisy, so for now it is enough to know it exists. If you want to look at only one value, echo $NAME is the quickest route.
Common confusion. The dollar sign belongs to reading, not to the name. The variable is called
HOME, and$HOMEmeans "the value ofHOME." When you set a variable (shown below), you write the name without the dollar sign.
Setting your own variable
You can create a variable by writing its name, an equals sign, and a value:
site=tidewatch
echo $site
tidewatch
There is one strict rule that surprises almost everyone: no spaces around the =. Compare these:
site=tidewatch # correct: sets the variable
site = tidewatch # wrong: the shell tries to run a command called "site"
With spaces, the shell reads site as the name of a program and = and tidewatch as its arguments. You will get a command not found message, which is a confusing way to be told you added extra spaces.
If the value contains spaces, wrap it in quotes so it stays one piece:
station="North Pier"
echo "$station"
Quoting gets its own careful treatment in module 1.13. For now, a safe habit is to put double quotes around both the assigned value (when it has spaces) and the $variable you read.
Shell-local versus exported
Here is the idea that separates a beginner from someone who understands the environment. There are really two layers:
- Shell variables. A plain assignment like
site=tidewatchcreates a variable that exists inside this shell only. Programs you launch from the shell cannot see it. - Environment variables. When you
exporta variable, you mark it so that every program the shell starts afterwards receives a copy of it.
site=tidewatch
export region=north
Now site is shell-local and region is exported. You can also export an existing variable afterwards:
site=tidewatch
export site
How do you see the difference? Start a child shell. A child shell is a fresh shell program launched from your current one, and it inherits only the exported variables:
sh -c 'echo "site is: $site"'
sh -c 'echo "region is: $region"'
site is:
region is: north
The first line prints nothing after the colon because site was never exported, so the child never received it. The second line shows north because region was.
Think of the shell as a person leaving for a trip. Exported variables are the items packed in the bag; everything else stays at home on the desk. A child process only gets what is in the bag.
Two further facts help keep the picture accurate:
- The copy is one way and one time. If a child changes its copy, your shell's value is unchanged. If you change the value later, a program that is already running does not notice.
- Exports last only as long as the shell session. Close the terminal window and they are gone, unless you recorded them in a startup file (see the section on dotfiles below).
To remove a variable entirely, use unset:
unset site
How the shell finds programs — PATH
Now the question from the start of the module. When you type ls, the shell does not search your whole disk. It consults one specific variable, PATH:
echo $PATH
/Users/sam/bin:/usr/local/bin:/usr/bin:/bin
PATH is a list of folders separated by colons. Reading the value above as a list, you get four folders:
/Users/sam/bin/usr/local/bin/usr/bin/bin
When you type a command name that has no slash in it, the shell walks this list from left to right. In each folder it asks, "is there an executable file here with this name?" The first match wins, the shell runs that file, and it stops searching. If it reaches the end of the list with no match, you see:
zzz: command not found
That message therefore does not mean "this program does not exist anywhere." It means "no folder on your PATH has an executable with that name."
Why the order matters
Suppose two folders each contain a program with the same name. The folder that appears earlier in PATH is the one that runs; the other is hidden behind it, a bit like two library books with the same title on different shelves where you always check the nearer shelf first. This is how people deliberately install a newer version of a tool: they place its folder at the front of the list so it wins over the older system copy.
Names with a slash skip the search
If the command contains a slash, the shell treats it as a path and does not consult PATH at all:
./scripts/sync_readings.sh
/bin/ls
This is why Sam had to write ./scripts/sync_readings.sh in module 1.5 rather than sync_readings.sh. The current folder . is not on PATH by design, so a bare name would produce command not found. Typing the ./ is you saying, "I mean this exact file, right here."
Asking "which one would run?" — which and command -v
Because of the search order, it is useful to ask the shell directly which file a name points to:
which ls
command -v ls
/bin/ls
/bin/ls
Both report the location the shell would use. The difference is subtle but handy:
whichis a separate program that searchesPATHand prints the first match. It is familiar and quick, but its behaviour varies a little between systems, and it only knows about files.command -vis built into the shell itself, so it reports what this shell would really do. It can also tell you about shell-internal commands (such ascd) that have no file on disk at all.
When you only need an answer for everyday use, either works. When you are debugging "why is the wrong version running?", prefer command -v. If you want to see every match instead of just the first, which -a ls lists them in order, which shows the hidden copies behind the winner.
If neither command prints anything for a name, the shell cannot find it on PATH. That is a strong clue about a command not found problem.
Adding to PATH
Because PATH is just a variable holding text, you extend it by building a new value from the old one:
export PATH="$HOME/bin:$PATH"
Read this right to left. $PATH is the old list, $HOME/bin: is a new folder plus a colon, and together they form a new list with ~/bin at the front. Putting it first means programs there beat same-named programs further along. To add a folder at the end instead, you would write export PATH="$PATH:$HOME/bin".
Two cautions deserve a clear mention:
- Always include the old
$PATH. If you writeexport PATH="$HOME/bin"you replace the whole list, and then evenlsstops working in that shell because its folder is no longer searched. Closing the terminal and opening a new one restores the default, so the mistake is recoverable, but it is startling the first time. - Folders earlier in the list are trusted first. Only place folders you control at the front.
Temporary overrides — VAR=value command
Sometimes you want a setting to apply to one command only and then disappear. Put the assignment directly in front of the command, on the same line, with no semicolon:
TZ=UTC date
date
The first date runs with the variable TZ set to UTC, so it prints the time in Coordinated Universal Time. The second date, a moment later, runs normally in your local time zone. The assignment lasted for exactly one command and was never added to your shell.
Compare it to the alternatives:
| Approach | Lifetime | Visible to the shell afterwards? |
|----------|----------|----------------------------------|
| VAR=value command | that one command | no |
| VAR=value (alone on a line) | rest of the session | yes, but shell-local |
| export VAR=value | rest of the session | yes, and passed to children |
The one-command form is gentle and safe, which makes it ideal for trying something out. Notice the neat consequence: because the variable exists in that command's environment, the program can read it even though it was never exported.
Dotfiles — where settings are kept (a light mention)
If exports vanish when you close the terminal, how do people keep their favorite settings? They write them into a startup file that the shell reads each time it begins. These files live in your home folder and their names start with a dot, which hides them from a plain ls. That is why they are called dotfiles.
| Shell | Typical startup file |
|-------|----------------------|
| zsh | ~/.zshrc |
| bash | ~/.bashrc (some systems also use ~/.bash_profile) |
A line such as export PATH="$HOME/bin:$PATH" placed in the right file takes effect in every new terminal. You can look at your own with ls -a ~ to reveal hidden files, and cat ~/.zshrc to read one.
We deliberately stop here. Shell configuration is a deep topic with many variations, and you do not need to edit anything to finish this course. The only takeaway is the reason such files exist: they replay your preferred settings for you at the start of each session. If you ever do edit one, make a backup copy first with cp, as you learned in module 1.4.
Putting it together — a short mental checklist
When a command misbehaves, work through these questions in order:
- Does the shell know the name? Run
command -v name. No output means "not onPATH." - Is the right copy winning? Run
which -a nameand compare the order withecho $PATH. - Is a setting missing? Run
echo $VARIABLEto see what the shell holds. An empty line means unset (or shell-local, if a child program is the one complaining). - Does the setting need to reach a child program? If so,
exportit, or apply it to one run withVAR=value command.
Summary
- The environment is a set of named values each shell carries; read one with
echo $NAME. HOME,USER, andPATHare three you will see constantly.- A plain assignment
name=valueis shell-local;exporthands the value to programs you launch. Never put spaces around=. - The shell finds commands by searching the folders in
PATHleft to right; the first match wins, and a name containing a slash skips the search. whichandcommand -vshow which file a name resolves to;command -vreflects the real shell behaviour.- Extend
PATHwithexport PATH="$HOME/bin:$PATH", always keeping the old value. VAR=value commandsets a variable for one command only.- Dotfiles such as
~/.zshrcand~/.bashrcreplay your settings at the start of every session.
Examples
Worked examples — environment and PATH in the tidewatch project
Sam has a tiny helper that prints a one-line status for the tidewatch project, and wants to understand why the shell can or cannot find it. Each scene gives a situation, the commands, an interpretation, and a takeaway. Your usernames and folders will differ; follow the shape of the output.
First, a small setup you can paste in. It creates two folders that each contain a helper called tide_status, plus a variable-reading script:
cd ~/cli-practice
mkdir -p tidewatch/tools_old tidewatch/tools_new
printf '#!/bin/sh\necho "status from OLD tools"\n' > tidewatch/tools_old/tide_status
printf '#!/bin/sh\necho "status from NEW tools"\n' > tidewatch/tools_new/tide_status
printf '#!/bin/sh\necho "station is: $STATION"\n' > tidewatch/tools_new/show_station
chmod u+x tidewatch/tools_old/tide_status tidewatch/tools_new/tide_status tidewatch/tools_new/show_station
Scene 1 — Reading three variables
Scene. Before anything else, Sam checks what the shell already knows.
echo $HOME
echo $USER
echo $PATH
/Users/sam
sam
/usr/local/bin:/usr/bin:/bin
Interpretation. The shell replaced each $NAME with its stored text, and echo printed the result. The third line is a list of three folders separated by colons: /usr/local/bin, then /usr/bin, then /bin. Whenever Sam types a bare command name, the shell will check those folders in that order.
Takeaway. echo $NAME is the quickest way to see a variable. If a value looks wrong or empty, you have found the first clue.
Scene 2 — Shell-local versus exported
Scene. Sam wants the show_station script to print the station name, so he sets a variable and runs the script.
STATION="North Pier"
echo "$STATION"
./tidewatch/tools_new/show_station
North Pier
station is:
Interpretation. In the shell itself, STATION has a value, so echo prints it. The script, however, runs as a separate program, and a plain assignment is not passed along. The script reads $STATION, finds nothing, and prints an empty value. Now Sam exports the variable and tries again:
export STATION
./tidewatch/tools_new/show_station
station is: North Pier
Interpretation. export marked STATION for inheritance, and the script received a copy. Nothing about the value changed; only whether other programs could see it.
Takeaway. If a program cannot see a variable you know you set, ask: was it exported? Remember the bag analogy: only packed items travel.
Scene 3 — A one-command override
Scene. Sam wants to try a different station name just once, without disturbing the exported one.
STATION="South Jetty" ./tidewatch/tools_new/show_station
./tidewatch/tools_new/show_station
echo "$STATION"
station is: South Jetty
station is: North Pier
North Pier
Interpretation. The assignment placed directly in front of the command applied only to that single run, so the script printed South Jetty. The second run used the regular value again. Even echo "$STATION" still shows North Pier, proving the temporary value never touched the shell.
Takeaway. VAR=value command is a safe way to experiment. It changes nothing that you have to undo afterwards.
Scene 4 — Two copies of one command, and the search order
Scene. There are two programs named tide_status in two folders. Sam first checks that the shell cannot yet find either.
command -v tide_status
tide_status
tide_status: command not found
The first command printed nothing, and the second produced the error. Neither folder is on PATH, so the shell never looked inside them. Now Sam adds both, new first:
export PATH="$HOME/cli-practice/tidewatch/tools_new:$HOME/cli-practice/tidewatch/tools_old:$PATH"
command -v tide_status
which -a tide_status
tide_status
/Users/sam/cli-practice/tidewatch/tools_new/tide_status
/Users/sam/cli-practice/tidewatch/tools_new/tide_status
/Users/sam/cli-practice/tidewatch/tools_old/tide_status
status from NEW tools
Interpretation. command -v reports the single winner, which is the copy in tools_new, because that folder is earlier in PATH. which -a shows both matches in search order, so Sam can see that the old copy still exists but is hidden behind the new one. Running tide_status therefore prints the NEW message.
Sam swaps the order to prove the point:
export PATH="$HOME/cli-practice/tidewatch/tools_old:$HOME/cli-practice/tidewatch/tools_new:$PATH"
tide_status
status from OLD tools
The same two folders are present as before; only their order changed, and the winner changed with it.
Takeaway. PATH is read left to right, and the first match wins. When the wrong version runs, compare which -a name against echo $PATH.
Scene 5 — Recovering from a replaced PATH
Scene. Sam makes a classic slip while typing quickly, replacing PATH instead of extending it.
export PATH="$HOME/cli-practice/tidewatch/tools_new"
ls
ls: command not found
Interpretation. The new value contains only one folder, so the shell no longer searches /bin or /usr/bin, which is where ls lives. The file is untouched; the shell merely cannot find it. Calling it by its full path still works, because a name with a slash skips the search:
/bin/ls tidewatch
archive data drafts notes scripts tools_new tools_old
The permanent repair is simply to open a new terminal window, because an export lasts only for the session. In the same window, Sam could also rebuild the value, but the safest habit is to extend rather than replace:
export PATH="$HOME/cli-practice/tidewatch/tools_new:$PATH"
Takeaway. A command not found for something as ordinary as ls usually means PATH has been overwritten. Keep :$PATH on the end of anything you write, and remember that a fresh terminal gives you a clean start.
Practice check — environment and PATH
Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.
1.What does `echo $HOME` print?
2.Sam types `site=tidewatch` and then runs a script that reads `$site`. The script sees an empty value. Why?
3.`PATH` is `/Users/sam/bin:/usr/bin:/bin`, and a program named `report` exists in both `/Users/sam/bin` and `/usr/bin`. Typing `report` runs…
4.Which command is built into the shell and reports what the shell would actually run for the name `ls`?
5.Sam wants `TZ` set to `UTC` for a single run of `date` and then forgotten. Which line does that?
6.Sam runs `export PATH="$HOME/tools"` and then `ls` reports `command not found`. What happened?
7.Why does Sam have to write `./scripts/sync_readings.sh` rather than just `sync_readings.sh` to run a script in the current project?