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:

  1. They're only needed in the shell, not other programs.
  2. 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.
  3. Attempting to pass PS1 to a new shell would typically fail, because the shell usually resets PS1.

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:

  1. you export PS1 as an environment variable, and
  2. it is set, but to the empty value, and
  3. 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.