Environment variables and PATH
Why a program you can run is still not found
A script can be present, executable, and owned by you, and the shell will still refuse to run it by name:
mkdir -p ~/bin
printf '#!/bin/sh\necho this is my own hello\n' > ~/bin/hello
chmod +x ~/bin/hello
hello
bash: hello: command not found
Nothing is wrong with the file. command not found is the shell reporting that it looked in the
directories it searches and ~/bin was not among them.
The environment is a copy, made at exec time
Every process gets a block of NAME=value strings from whatever started it. The kernel hands that
block over when the program is executed, and from then on it belongs to the new process. Changes a
child makes are changes to its own copy.
This one fact explains most of the surprising behaviour. A variable set in your shell is not automatically part of that block:
GREETING=hello
echo "$GREETING"
bash -c 'echo "child sees: [$GREETING]"'
hello
child sees: []
GREETING exists as a shell variable, visible to the shell itself and to nothing it starts.
export moves it into the block that children receive:
export GREETING=hello
bash -c 'echo "child sees: [$GREETING]"'
child sees: [hello]
You can also set one for a single command, without affecting the shell at all, by putting the assignment in front of the command:
GREETING=hello bash -c 'echo "just for this command: [$GREETING]"'
just for this command: [hello]
Because the copy only ever travels downward, a script cannot change the environment of the shell that ran it:
printf '#!/bin/sh\nexport CHANGED=yes\n' > setit.sh
chmod +x setit.sh
./setit.sh
echo "parent sees: [${CHANGED:-nothing}]"
parent sees: [nothing]
Sourcing the file with . runs those lines in the current shell instead of a new one, which is
why installers tell you to run . ~/.profile rather than executing it:
printf 'export CHANGED=yes\n' > setit.sh
. ./setit.sh
echo "parent sees: [${CHANGED:-nothing}]"
parent sees: [yes]
How the shell finds a command
PATH holds directories separated by colons. When you type a command with no slash in it, the
shell tries each directory in turn and runs the first match it finds, though only after it has
ruled out an alias, a function and a builtin of the same name
(which vs type vs command -v):
printenv PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
type reports what a name resolves to, and type -a lists every candidate in search order:
type ls
type -a echo
ls is /usr/bin/ls
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo
Two things are worth reading off that output. A builtin beats anything on disk, so echo never
reaches /usr/bin/echo from an interactive shell. And /usr/bin/echo comes before /bin/echo
because /usr/bin comes first in PATH, not because one is more real than the other.
Position therefore decides which copy of a name you get. Prepending a directory shadows the system version; appending it provides a fallback for names nothing else supplies:
mkdir -p ~/bin
printf '#!/bin/sh\necho not the real one\n' > ~/bin/sort
chmod +x ~/bin/sort
PATH="$HOME/bin:$PATH" command -v sort
PATH="$PATH:$HOME/bin" command -v sort
/home/user/bin/sort
/usr/bin/sort
This completely fixes the opening example:
mkdir -p ~/bin
printf '#!/bin/sh\necho this is my own hello\n' > ~/bin/hello
chmod +x ~/bin/hello
PATH="$HOME/bin:$PATH" hello
this is my own hello
Keep $PATH on the right-hand side when you extend it. Writing PATH="$HOME/bin" alone replaces
the list, so the next command you type will not be found either.
Where PATH is set on Debian
The value you see depends on how the shell was started, which is why a change can appear to work in one terminal and not in another:
bash -lc 'echo $PATH'
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
That is a login shell, built from /etc/profile and ~/.profile. The non-login interactive
shell above had a different list, inherited from whatever started it. Interactive is a third
question again, answered by
whether the shell has a terminal. On Debian:
/etc/environmentis read by PAM for every login session. It is a plain list ofNAME=valuelines, not a script, soPATH="$PATH:/opt/bin"does not work there./etc/profileand~/.profilerun for login shells.~/.profileis where a user's ownPATHaddition belongs, and Debian's default already adds~/binand~/.local/binif they exist, which is why adding a directory to your PATH often means creating one rather than editing anything.~/.bashrcruns for interactive non-login shells. Debian's default~/.profilesources~/.bashrc, which is why edits there often seem to work for both.- A systemd service inherits almost nothing from any of these.
Units get a minimal environment and need
Environment=orEnvironmentFile=set explicitly. - cron is the same story and catches more people, because a job that works
when you type it fails at 03:00 with no
PATHworth the name.
Log out and back in after editing any of them. Sourcing the file only fixes the shell you are sitting in.
sudo does not use your PATH
sudo builds a fresh environment rather than passing yours through, and Debian compiles it with
secure_path set:
sudo grep secure_path /etc/sudoers
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
So sudo searches that list and no other:
sudo printenv PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
A binary in ~/bin or /opt/something/bin is invisible to sudo however well it works for you,
which produces the confusing pairing of a command that runs fine and then reports
command not found the moment you put sudo in front of it. Give the full path, or use
sudo env "PATH=$PATH" thecommand when you have decided that is acceptable.
sudo: command not found covers the diagnosis in more
detail.
Common misconceptions
export is not global. It affects processes started afterwards, from this shell. Other
terminals, running programs and your desktop session are unaffected, and there is no supported way
to push a variable into a process that is already running.
An empty variable and an unset one are different. ${VAR:-default} supplies the default for
both, ${VAR-default} only for genuinely unset, and printenv VAR prints an empty line for the
first and nothing for the second. Parameter expansion has the
rest of that family, including ${VAR:?message} for the variables a script cannot guess at.
Quote the expansion. export DIR=$HOME/my documents sets DIR to something that ends at the
space. See variables and quoting for the general rule, which
applies here exactly as it does anywhere else.
Never put . in PATH. It makes the shell run a program from whatever directory you happen
to be sitting in, which turns a downloaded archive containing a file called ls into a trap. If
you want the current directory, ./thing says so explicitly.
Go deeper
- variables and quoting for shell variables in scripts
- sudo: command not found for the
secure_pathcase - pipes and redirection for the other thing a process inherits at exec time
- file permissions explained for the executable bit that
PATHsearch assumes