Sooner or later it happens to everyone: you install a program, run it from the terminal and the shell replies «command not found», even though the file is right there. Or you follow a guide that says to «add a line to your bashrc» without explaining why. Behind both cases is the same mechanism, environment variables, and it is one of the concepts that makes everything else simpler.
What an environment variable is
Every process receives, when it starts, a small set of name-value pairs: its environment. Programs read them to know, for example, where the user's home folder is, which language to use or which editor to open. Some of the most common:
| Variable | Contents |
|---|---|
HOME |
the user's personal folder |
USER |
the user's name |
PATH |
the folders in which to look for commands |
LANG |
language and regional format |
EDITOR |
the default editor for programs that open one |
SHELL |
the login shell |
To see them all, use env or printenv; for just one, echo $HOME.
Shell variables and exported variables
In the shell you can create a variable with an assignment, without spaces around the equals sign:
PROJECT=/srv/site
echo $PROJECT
This variable, however, stays in the current shell. If you launch a program or a script, it can't see it. To pass it on to child processes you have to export it:
export PROJECT=/srv/site
This distinction explains plenty of mysterious behaviour: a script that «can't find» a variable you just set is usually looking for one that was never exported. Remember too that the environment is only inherited downwards: a child process cannot change its parent's environment. That is why a script that runs export doesn't change the terminal you launched it from, unless you run it with source script.sh.
To set a variable for a single command only, there is a compact form:
LANG=C ls -l
Handy, for example, to get messages in English, which are easier to search for when diagnosing an error.
How PATH works
When you type ls, the shell doesn't know where that program is. It looks at the PATH variable, a list of folders separated by colons:
echo $PATH
/usr/local/bin:/usr/bin:/bin:/home/anna/.local/bin
and tries them in order, stopping at the first one containing an executable file with that name. Two practical consequences follow:
- a program in a folder that isn't listed won't be found, and must be run with its full path, such as
./script.shor/opt/tool/bin/tool; - if two folders contain a command with the same name, the one listed first wins.
To find out which version of a command is actually used, run type name or command -v name. The former also tells you whether it is an alias or a shell function.
To add a folder to PATH, put it at the end, or at the front if it must take precedence:
export PATH="$HOME/.local/bin:$PATH"
The classic mistake is writing PATH=$HOME/bin without the trailing :$PATH: the shell loses all the other folders and stops finding even ls. If that happens, in the current session you fix it with export PATH=/usr/bin:/bin and then correct the file.
.bashrc, .profile and the rest
Variables set by hand last until the terminal is closed. To make them stick, you write them in a file read when the shell starts. Which file depends on the kind of shell:
- login shell (logging in from a console or via SSH): Bash reads
~/.bash_profileor, if it doesn't exist,~/.profile; - interactive non-login shell (a new terminal on the desktop): Bash reads
~/.bashrc.
On many distributions ~/.profile in turn includes ~/.bashrc, so the settings apply everywhere. A sensible split: environment variables such as PATH and EDITOR in ~/.profile; aliases, functions and the look of the prompt in ~/.bashrc. On macOS the default shell has been zsh for years, which uses ~/.zshrc and ~/.zprofile.
After a change there is no need to close the terminal:
source ~/.bashrc
Settings that apply to all users live in /etc/profile, /etc/profile.d/ and /etc/environment.
Useful aliases
Aliases shorten long commands or add safety options:
alias ll='ls -lh'
alias rm='rm -i'
alias update='sudo apt update && sudo apt upgrade'
In my work I prefer a few clear aliases: an over-customised terminal becomes hard to use when you are on a machine that isn't yours, where none of those aliases exist.
Secrets in variables: with care
It is common to pass access keys to programs through environment variables. That's fine, but be careful: exported variables are visible to child processes and, for the same user, readable in /proc/<PID>/environ. They shouldn't be written into .bashrc if the file ends up in a repository or a shared backup, and a command like export TOKEN=... typed by hand stays in the shell history.
In short
- The environment is a set of variables every process inherits from its parent.
- Without
exporta variable stays in the current shell and scripts can't see it. - PATH is read in order; when adding a folder, always keep
:$PATH. ~/.profilefor variables,~/.bashrcfor aliases and prompt; on macOS~/.zshrc.typeandcommand -vreveal which command is really being run.