>_ Operating systems & Linux

Environment variables and PATH: configuring the shell with .bashrc

Environment variables on Linux and macOS: how PATH works, .bashrc versus .profile, useful aliases and the common mistakes that break a terminal.

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.sh or /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_profile or, 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 export a variable stays in the current shell and scripts can't see it.
  • PATH is read in order; when adding a folder, always keep :$PATH.
  • ~/.profile for variables, ~/.bashrc for aliases and prompt; on macOS ~/.zshrc.
  • type and command -v reveal which command is really being run.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. With degrees in computer science and psychology, he combines technical analysis with an understanding of the human factor.

Read also