Why aren't variables like $PS1 in printenv?
That's because PS1
is not normally exported.
Environment variables are used to set the execution environment of child processes; since PS1
only really has significance within an interactive shell, there's not normally any point exporting it - it is just a plain shell variable.
If you start an interactive child shell, then it will read and set its PS1
from the shell's resource file such as ~/.bashrc
If you export PS1
then you will see it in the printenv
output. Alternatively you can see plain shell variables using the bash builtin set
as described here How to list all variables names and their current values?
Is there a more comprehensive output command that does more than
printenv
?
printenv
prints only environment variables, which may be considered an advantage. But if you want to print shell variables as well, use echo "$x"
(or printf '%s\n' "$x"
, which is more robust) instead of printenv x
.
steeldriver's explanation of these issues is useful and correct, but I'm presenting the topic in another way here.
printenv
is an external command--not built into your shell, but a separate program from your shell. It shows its own environment variables, which are those it inherits from the shell you use to run it. However, shells don't pass all their variables into their subprocesses' environments. Instead they maintain a distinction between which variables are environment variables and which are not. (Those that are not are often call shell variables.)
Shell Variables
To see how this works, try these commands, which are enclosed in (
)
so they act independently1 of one another. Individually, each of these commands works the same when you run it without the (
)
, but variables you create in earlier commands would still exist in later commands. Running the commands in subshells prevents this.
Creating a new variable, then running an external command, does not pass the variable into the command's environment. Except in the unusual case that you already have an environment variable x
, this command produces no output:
(x=foo; printenv x)
The variable is assigned in the shell, though. This command outputs foo
:
(x=foo; echo "$x")
The shell supports syntax to pass a variable into a command's environment without affecting the current shell's environment. This outputs foo
:
x=foo printenv x
(That works in a subshell, too, of course--(x=foo printenv x)
--but I've shown it without the (
)
because when you use that syntax, nothing is set for your current shell, so using a subshell is unnecessary to prevent subsequent commands from being affected.)
This prints foo
, then prints bar
:
(x=bar; x=foo printenv x; echo "$x")
Exporting
When you export a variable, it is automatically passed into the environments of all subsequent external commands run from the same shell. The export
command does this. You can use it before you define the variable, after you define it, or you can even define the variable in the export
command itself. All these print foo
:
(x=foo; export x; printenv x)
(export x; x=foo; printenv x)
(export x=foo; printenv x)
There is no unexport
command. Even though you can export a variable before setting it, unsetting a variable also unexports it, which is to say that this prints nothing, rather than printing bar
:
(x=foo; export x; unset x; x=bar; printenv x)
But changing the value of a variable after exporting it does affect the exported value. This prints foo
, then bar
:
(export x=foo; printenv x; x=bar; printenv x)
Like other processes, your shell itself inherits environment variables from its parent process. Such variables are present initially in your shell's environment and they are automatically exported--or remain exported, if you choose to think of it that way. This prints foo
(remember, VAR=val cmd
runs cmd
with VAR
set to val
in its environment):
x=foo bash -c 'printenv x'
Variables set in child processes do not affect the parent process, even if they are exported. This prints foo
(not bar
):
(x=foo; bash -c 'export x=bar'; echo "$x")
Subshells
A subshell is also a child process2; this also prints foo
:
(x=foo; (export x=bar); echo "$x")
That should make clearer why I've enclosed most of these commands in (
)
to run them in subshells.
Subshells are special, though. Unlike other subprocesses, such as those created when you run an external command like printenv
or bash
, a subshell inherits most of its parent shell's state. In particular, subshells inherit even variables that aren't exported. Just as (x=foo; echo "$x")
prints foo
, so does (x=foo; (echo "$x"))
.
The unexported variable is still not exported in the subshell--unless you export it--so, just as (x=foo; printenv x)
prints nothing, so does (x=foo; (printenv x))
.
A subshell is a special kind of subprocess that is a shell. Not all subprocesses that are shells are subshells. The shell created by running bash
is not a subshell and it does not inherit unexported variables. So this command prints a blank line (because echo
prints a newline even when called with an empty argument):
(x=foo; bash -c 'echo "$x"')
Why PS1
isn't an environment variable (and usually shouldn't be one)
Finally, as for why prompt variables like PS1
are shell variables but not environment variables, the reasons are:
- They're only needed in the shell, not other programs.
- They are set for each interactive shell, and noninterative shells don't need them at all. That is, they don't need to be inherited.
- Attempting to pass
PS1
to a new shell would typically fail, because the shell usually resetsPS1
.
Point #3 deserves a bit more explanation, though if you never attempt to make PS1
an environment variable, then you probably don't really need to know the details.
When Bash starts noninteractively, it unsets PS1
.
When a noninteractive Bash shell starts up, it always3unsets PS1
. This prints a blank line (not foo
):
PS1=foo bash -c 'echo "$PS1"'
To verify that it is actually unset, and not just set but empty, you can run this, which prints unset
:
PS1=foo bash -c 'if [[ -v PS1 ]]; then echo set; else echo unset; fi'
To verify that this is independent of other startup behavior, you could try passing any combination of --login
, --norc
, or --posix
before -c
, or setting BASH_ENV
to the path of some script (e.g., BASH_ENV=~/.bashrc PS1=foo bash ...
), or ENV
if you passed --posix
. In no case does a noninteractive Bash shell fail to unset PS1
.
What this means is that if you export PS1
and run a non-interactive shell which itself runs an interactive shell, it won't set have the PS1
value you originally set. For this reason--and also because other shells besides Bash (like Ksh) don't all behave the same way, and the way you write PS1
for Bash does not always work for those shells--I recommend against attempting to make PS1
an environment variable. Just edit ~/.bashrc
to set whatever prompt you want.
When Bash starts interactively, it often sets or resets PS1
.
Conversely, if you unset PS1
and run an interactive Bash shell, even if you prevent it from running commands from startup scripts by passing --norc
, it will still automatically set PS1
to a default value. Running env -u PS1 bash --norc
gives you an interactive Bash shell with PS1
set to \s-\v\$
. Since Bash expands \s
to the name of the shell and \v
to the version number, this shows bash-4.3$
as the prompt on Ubuntu 16.04 LTS. Note that setting PS1
's value as the empty string is not the same as unsetting it. As explained below, running PS1= bash
gives you an interactive shell with strange startup behavior. You should avoid exporting PS1
when it is set to the empty string, in practical use, unless you understand and want that behavior.
However, if you set PS1
and run an interactive Bash shell--and it doesn't get unset by an intermediary noninteractive shell--it will keep that value... until a startup script like the global /etc/profile
(for login shells) or /etc/bash.bashrc
, or your per-user ~/.profile
, ~/.bash_login
, or ~/.bash_profile
(all for login shells) or ~/.bashrc
resets it.
Even if you edit those files to prevent them from setting PS1
--which, in the case of /etc/profile
and /etc/bash.bashrc
, I recommend against doing anyway, since they affect all users--you can't really rely on this. As mentioned above, interactive shells started from noninteractive shells won't have PS1
, unless you were to reset and reexport it in the noninteractive shell. Furthermore, you should think twice before doing that, because it is common for shell code (including shell functions you may have defined) to check PS1
to determine whether the shell it's running in is interactive or noninteractive.
Checking PS1
is a common way to determine if the current shell is interactive.
This is why it is so important for noninteractive Bash shells4 to unset PS1
automatically. As section 6.3.2 Is this Shell Interactive? of the Bash reference manual says:
[S]tartup scripts may examine the variable
PS1
; it is unset in non-interactive shells, and set in interactive shells.
To see how this works, see the example there. Or check out the the real-world uses in Ubuntu. By default, /etc/profile
in Ubuntu includes:
if [ "$PS1" ]; then
if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then
# The file bash.bashrc already sets the default PS1.
# PS1='\h:\w\$ '
if [ -f /etc/bash.bashrc ]; then
. /etc/bash.bashrc
fi
else
if [ "`id -u`" -eq 0 ]; then
PS1='# '
else
PS1='$ '
fi
fi
fi
/etc/bash.bashrc
, which should do nothing at all when the shell is noninteractive, has:
# If not running interactively, don't do anything
[ -z "$PS1" ] && return
Subtleties of different methods of checking for interactivity:
To achieve the same goal, /etc/skel/.bashrc
, which is copied into users' home directories when their accounts are created (so your ~/.bashrc
is probably similar), has:
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
That's the other common way to check if a shell is interactive: see if the text obtained by expanding the special parameter -
(by writing $-
) contains the letter i
. Usually this has exactly the same effect. Suppose, however, that you have not modified the code shown above that appears by default in Bash's startup scripts in Ubuntu, and that:
- you export
PS1
as an environment variable, and - it is set, but to the empty value, and
- you start an interactive Bash shell...
Then /etc/profile
(if it is a login shell) or /etc/bash.bashrc
won't run the commands they usually run for interactive shells. ~/.bashrc
still will.
If you want to check if a shell is interactive by using PS1
and get the right answer even when PS1
is set but empty, you can use [[ -v PS1 ]]
or [ -v PS1 ]
/test -v PS1
instead. Note, however, that the [[
keyword, and the -v
test of the [
and test
shell builtins, are particular to Bash. Not all other Bourne-style shells accept them. So you should not use them in scripts like ~/.profile
and /etc/profile
that might run in other shells (or by a display manager when you log in graphically), unless you have something else in the script that checks what shell is running and only executes Bash-specific commands when that shell is Bash (for example, by checking $BASH_VERSION
).
Notes
1This article explains subshells in detail. 3.2.4.3 Grouping Commands of the Bash reference manual explains the (
)
syntax.
2 Note that there are circumstances under which commands run in subshells even with the (
)
syntax is not used. For example, when you have commands separated by |
in a pipeline, Bash runs each of them in a subshell (unless the lastpipe
shell option is set).
3 Except for subshells. Arguably that is not even an exception, since subshells don't "start up" in the usual sense that we mean when we talk about that. (They don't really have significant initialization behavior.) Note that when you run bash
--with or without arguments--inside a Bash shell, that creates a subprocess that is a shell, but it is not a subshell.
4 Note that not all shells--not even all Bourne-style shells--behave this way. But Bash does, and it is very common for Bash code, including code in startup scripts, to rely on it.