35.1 Environment Variables: What They Are and How Processes Inherit Them
Right, let’s talk about environment variables. Forget the dry, academic definition for a moment. Think of them as little sticky notes your operating system slaps onto every program you run. When a program (or “process,” if we’re being formal) needs to know something about the world it’s running in—like where to find your documents, what your username is, or which language you prefer—it just checks these sticky notes. It’s a brilliantly simple system for configuration and communication, and it’s one of the first things you need to wrap your head around.
The key to their power, and the source of most headaches, is how they’re inherited. When you start a new process—be it from your terminal, your desktop launcher, or by another program—it doesn’t start with a blank slate. It gets a copy of the environment variables from its parent process. This is a copy, not a live link. This is a critical distinction. If you change an environment variable in a child process, that change dies with that process. The parent’s environment remains untouched. This is why you can’t just export DATABASE_URL="my_url" in a shell script and expect your main shell to know about it when the script ends—the script was a child process that made a change to its own copy, which then vanished.
The View from the Command Line: env, printenv, and echo
Before we start slinging variables around, let’s see what’s already in there. Your shell session right now has dozens of these sticky notes. To see them all, the env or printenv command is your best friend.
env
This will spit out a list of all your current environment variables, like USER, HOME, PATH, and SHELL. It’s a firehose of information. To look for a specific one, printenv is slightly more precise, or you can use the trusty echo command, prefixing the variable name with a $ so the shell knows to substitute its value.
printenv HOME
# Or, more commonly:
echo $HOME
Setting and Exporting: The Nitty-Gritty
Here’s where the shell gets interesting. You can set a variable easily enough, but whether child processes see it is another matter.
In bash and similar shells, a simple assignment creates a shell variable.
MY_VAR="This is a test"
This variable lives only in your current shell. If you run a script now, it won’t see MY_VAR. It’s a sticky note you’ve written but haven’t stuck to anything. To make it an environment variable—to actually stick it to the current shell process so all its children inherit it—you need to export it.
export MY_VAR
# Or, do it all in one line:
export MY_OTHER_VAR="This will be inherited"
Now, any process you start from this shell will have a copy of MY_VAR and MY_OTHER_VAR in its environment. This is the fundamental mechanism behind your shell configuration files (like .bashrc); they run commands and export variables to set up your environment for every subsequent shell and program.
The PATH:
The PATH:
Your Shell’s Recipe Book
If there’s one environment variable to rule them all, it’s PATH. This is a perfect example of the “why.” When you type a command like ls, your shell doesn’t magically know what code to run. It has a list of directories (the PATH) where it goes to look for an executable file named ls. It checks each directory in order, and runs the first one it finds.
Let’s see what’s in yours. It’s a colon-separated list.
echo $PATH
# /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
This means when you type python, it first looks in /usr/local/bin, then /usr/bin, and so on. This is why you can install a newer version of a tool in /usr/local/bin and it will be found before the system version in /usr/bin. It’s also why you get “command not found” errors; the shell exhausts every directory in PATH and finds nothing.
To add a new directory (say, ~/my_scripts) to your PATH for the current session, you export the modified list:
export PATH="$HOME/my_scripts:$PATH"
Notice we prepend it ($NEW_DIR:$OLD_PATH). This ensures our custom script directory is searched first, which is usually what you want. Appending it ($OLD_PATH:$NEW_DIR) means system directories take precedence.
The Inheritance Chain: It’s a Family Affair
Let’s visualize this inheritance with a quick example. We’ll create a simple script that shows its environment.
#!/bin/bash
# Let's call this script 'env_demo.sh'
echo "The value of MY_INHERITED_VAR is: ${MY_INHERITED_VAR:-'Not Set'}"
Make it executable (chmod +x env_demo.sh). Now, let’s play it out:
# In your main shell
export MY_INHERITED_VAR="Hello"
./env_demo.sh # The script will output: "The value of MY_INHERITED_VAR is: Hello"
# But if you run it without exporting first...
MY_INHERITED_VAR="Hello"
./env_demo.sh # The script will output: "The value of MY_INHERITED_VAR is: Not Set"
See? The child only gets what you’ve explicitly exported. This design is why it’s safe; a poorly written script can’t mess up your main shell’s environment. It’s also the reason you need to source files (like source venv/bin/activate) instead of running them, as sourcing runs the commands in the current shell process, avoiding the whole parent-child copy problem.