Any time you type a command in your terminal, your shell will try to locate that command on the filesystem. If found, your shell will run the command. Otherwise, you’ll typically see a “command not found” error or similar. You already know this.
But where exactly is your shell looking for the command? It searches the "list" of the directories specified in your PATH environment variable:
> echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
In the example above, you probably noticed that the "list" is actually a long string of colon-delimited directories. This is commonly referred to as the "search path." Your shell will search each directory, in the order that they’re specified, stopping as soon as it finds the command you're attempting to run.
This process is what makes it possible to have multiple versions of the same program installed, such as python3. For example, macOS Tahoe ships with Python 3.9 pre-installed to /usr/bin/python3. But I want to use Python 3.12, so I use my favorite package manager, Homebrew, to brew install python@3.12. This leaves me with two versions of python3 on my system:
/usr/bin/python3/opt/homebrew/bin/python3
Which one will run when I type python3? You can find out using the which command:
> which python3
/usr/bin/python3
The outdated, pre-installed version will run because /usr/bin appears in my search path, and /opt/homebrew/bin is not present at all, as seen above in my PATH output.
To workaround this, you could type the full, absolute path to the newer version:
> /opt/homebrew/bin/python3
Python 3.13.5 (main, Jun 11 2025, 15:36:57) [Clang 17.0.0 (clang-1700.0.13.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
The more idiomatic approach is to modify your PATH environment variable so that /opt/homebrew/bin is also in the search path. It's also important to ensure that it appears sooner than /usr/bin, so that your shell looks there first:
export PATH="/opt/homebrew/bin:$PATH"
This takes the existing PATH value and prepends it with /opt/homebrew/bin, before assigning the new value back to PATH. The export directive makes the updated PATH value available in any child processes or subshells.
> export PATH="/opt/homebrew/bin:$PATH"
> echo $PATH
/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
> which python3
/opt/homebrew/bin/python3
Manually typing it like this is a one-off override for your current shell session only. To persist it so that you don't need to do this again, put export PATH=... in your shell configuration file, such as .bashrc, .zshrc, or similar depending on which shell you use.
Get familiar and comfortable with checking and manipulating your PATH. Over time, as you install things, your search path will get messy and it's inevitable that at some point you'll need to go in and fix it yourself.
echo vs printenv
echo
To see the value of PATH or any environment variable, most people typically use echo:
> echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
When using this method, the dollar sign is a special character that tells your shell to replace $PATH with its literal value. That literal value is then passed as an argument to echo. As its name implies, echo simply outputs or echos whatever you pass to it.
One gotcha is that echo always returns 0 , even if the environment variable doesn’t exist, because echoing nothing is a still valid operation for echo.
> echo $BLAH
> echo $?
0
In the example above, I used $? to output the return/exit code of the last command. Zero means success, non-zero means failure.
Still, using echo is completely fine for quickly inspecting environment variables.
printenv
If you need the value of an environment variable and you want to validate that there wasn’t an error retrieving it, such as when writing scripts for automation, I would encourage you to use printenv instead:
> printenv PATH
/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
> echo $?
0
There is no dollar sign, so nothing will be replaced. We are passing the literal string “PATH” to the command printenv. It will then check that the variable exists (case-sensitive), and if so, returns its value. If not, there will be no output and the return code will be 1, indicating an error retrieving the value.
> printenv BLAH
> echo $?
1
NOTE: One important caveat is that printenv can only display environment variables and exported variables. To output local variables, you need to use echo. Some might consider printenv to be "safer" when you're checking specifically for environment variables. There are other ways to test for values using conditionals expressions, however, they are similar to echo in that they can't distinguish between local and environment/exported variables.
On Using GenAI to Manage Your PATH
Don't. No matter how good you think your GenAI utility is at managing your shell configuration files, you need to understand how the PATH works and how to fix it yourself. After you install enough programs, your PATH will become messy and contribute to producing unexpected results at some point. No developer can avoid this with GenAI; it's a human cost. So don't waste your requests/tokens repairing something that you can easily repair yourself. Lastly, you haven't even seen the PATH's final form (spoiler: I'll cover Python's virtual environments in the third post of this series 🙂).
Relevant Music
You can't avoid the human cost that must be paid. Learn the fundamentals, follow the PATH, be more productive.