109

I read here that the purpose of export in a shell is to make the variable available to sub-processes started from the shell.

However, I have also read here and here that "Processes inherit their environment from their parent (the process which started them)."

If this is the case, why do we need export? What am I missing?

Are shell variables not part of the environment by default? What is the difference?

5 Answers 5

124

Your assumption is that shell variables are in the environment. This is incorrect. The export command is what defines a name to be in the environment at all. Thus:

a=1 b=2
export b

results in the current shell knowing that $a expands to 1 and $b to 2, but subprocesses will not know anything about a because it is not part of the environment (even in the current shell).

Some useful tools:

  • set: Useful for viewing the current shell's parameters, exported-or-not
  • set -k: Sets assigned args in the environment. Consider f() { set -k; env; }; f a=1
  • set -a: Tells the shell to put any name that gets set into the environment. Like putting export before every assignment. Useful for .env files, as in set -a; . .env; set +a.
  • export: Tells the shell to put a name in the environment. Export and assignment are two entirely different operations.
  • env: As an external command, env can only tell you about the inherited environment, thus, it's useful for sanity checking.
  • env -i: Useful for clearing the environment before starting a subprocess.

Alternatives to export:

  1. name=val command # Assignment before command exports that name to the command.
  2. declare/local -x name # Exports name, particularly useful in shell functions when you want to avoid exposing the name to outside scope.
  3. set -a # Exports every following assignment.

Motivation

So why do shells need to have their own variables and and environment that is different? I'm sure there are some historical reasons, but I think the main reason is scoping. The enviroment is for subprocesses, but there are lots of operations you can do in the shell without forking a subprocess. Suppose you loop:

for i in {0..50}; do
    somecommand
done

Why waste memory for somecommand by including i, making its environment any bigger than it needs to be? What if the variable name you chose in the shell just happens to mean something unintended to the program? (Personal favorites of mine include DEBUG and VERBOSE. Those names are used everywhere and rarely namespaces adequately.)

What is the environment if not the shell?

Sometimes to understand Unix behavior you have to look at the syscalls, the basic API for interacting with the kernel and OS. Here, we're looking at the exec family of calls, which is what the shell uses when it creates a subprocess. Here's a quote from the manpage for exec(3) (emphasis mine):

The execle() and execvpe() functions allow the caller to specify the environment of the executed program via the argument envp. The envp argument is an array of pointers to null-terminated strings and must be terminated by a NULL pointer. The other functions take the environment for the new process image from the external variable environ in the calling process.

So writing export somename in the shell would be equivalent to copying the name to the global dictionary environ in C. But assigning somename without exporting it would be just like assigning it in C, without copying it to the environ variable.

7
  • 3
    set -k is so that one can use cmd ENVVAR=value in place of ENVVAR=value cmd, that won't work in your example unless set -k was run prior to invoking f. Also, not many shells support it nowadays and only for backward compatibility with the Bourne shell. In the Bourne (or Korn) shell, that wouldn't work for functions. And because it affects shell parsing, it has to be in effect at the time the shell reads the code that makes use of it there. Nov 7, 2017 at 9:49
  • 1
    You may also want to mention set -a Nov 7, 2017 at 9:50
  • This is a helpful answer but I feel like it should also address how/why shell variables are different than environment variables, 'cuz that's super confusing.
    – Mike B
    Jun 11, 2020 at 21:33
  • @mblakesley ok, edited. Is that what you're looking for?
    – kojiro
    Aug 8, 2020 at 22:30
  • 2
    @mblakesley OK, I tried again.
    – kojiro
    Aug 10, 2020 at 12:57
34

There's a difference between shell variables and environment variables. If you define a shell variable without exporting it, it is not added to the processes environment and thus not inherited to its children.

Using export you tell the shell to add the shell variable to the environment. You can test this using printenv (which just prints its environment to stdout, since it's a child-process you see the effect of exporting variables):

#!/bin/sh

MYVAR="my cool variable"

echo "Without export:"
printenv | grep MYVAR

echo "With export:"
export MYVAR
printenv | grep MYVAR
6

A variable, once exported, is part of the environment. PATH is exported in the shell itself, while custom variables can be exported as needed. Using some setup code:

$ cat subshell.sh 
#!/usr/bin/env bash
declare | grep -e '^PATH=' -e '^foo='

Compare

$ cat test.sh 
#!/usr/bin/env bash
export PATH=/bin
export foo=bar
declare | grep -e '^PATH=' -e '^foo='
./subshell.sh
$ ./test.sh 
PATH=/bin
foo=bar
PATH=/bin
foo=bar

With

$ cat test2.sh 
#!/usr/bin/env bash
PATH=/bin
foo=bar
declare | grep -e '^PATH=' -e '^foo='
./subshell.sh
$ ./test2.sh 
PATH=/bin
foo=bar
PATH=/bin

Since foo is not exported by the shell, and test2.sh never exported it, it was not part of the environment of subshell.sh in the last run.

3

It's not about the parent / child relationship.

When a process forks a child, the child is an almost exact copy of the parent, so it inherits everything.

In:

$ var=1; (echo "$var"; var=2); echo "$var"
1
1

Where most shells implement the (...) subshell environment by forking a child process, that child process has access to the same data as the parent.

The environment variables are about preserving data (in that case key=value strings) across execution of a new command within a same process.

When you do:

$ export VAR=value
$ printenv VAR
value

The shell forks a child process (which inherits everything), but then in that child process executes the printenv command (and with exec printenv VAR, the forking is skipped).

It's only at that point that the memory of that child process is completely wiped before loading the new executable, so everything is lost. The environment variables are passed in the third argument of the execve("/usr/bin/printenv", ["printenv", "VAR"], environ) system call.

The shell places the VAR=value inside that environ array of strings for printenv to retrieve it, and in Bourne-like shells, only the variables that have been marked with the export attributes are put there (with some variation in behaviour when those variables are not scalar variables).

Some shells like rc and derivatives put all their variables in the environment even the array ones (which they need to encode some specific way which is only understood by other instances of themselves).

The Bourne shell had env vars even more separated from shell vars in that shell variables were created from env var on startup, but you had to export those shell variables even if they were initially imported from the environment for any modification made to them to be propagated to executed commands. It's similar in the C-shell, where you have to use setenv instead of set to set values of variables if you want the modification to be exported to executed commands.

ksh also exports the attributes of exported variable via the special A__z env var. bash can also export its options to other executed bash instances via the SHELLOPTS and BASHOPTS environment variables.

In perl, the environment variables are mapped to the %ENV associative array variable.

Note that env var names can contain any sequence of bytes other than = and NUL, and the third argument of execve() can contain more than one definition for a same variable and even strings that don't contain = characters, while shells have much greater restrictions as to what their variable names may contain. You'll find some variation in behaviour as to what shells do with environment strings that can't be mapped to shell variables.

3

In summary:

  • processes DO inherit environment variables from their parents
  • in bash, $a=1 creates a 'normal' variable, not an environment variable
  • export a=1 creates an environment variable for the process of the terminal (there are also other ways to do it)
  • there are other ways to pass environment data to a child process
2
  • in Bash or any other POSIXy shell, $a=1 doesn't create any variables, a=1 does. Also, "for the process of the terminal" sounds, well, confusing? I guess you mean "the process running at the foreground at the time on that terminal", but even then, that's a shell command, you can't just give to any process. Then, I also wonder if this answer really adds anything to the four earlier ones.
    – ilkkachu
    Apr 14, 2021 at 12:03
  • I thought that the variable is created (most likely added to a dictionary) in the process executing the command, which, I figured would be the process running the terminal. What I aimed with this answer is (as the first line states) a summary, a not so deep answer to the question. Thank you for the critique, I will look into how commands are executed in the terminal. Apr 15, 2021 at 13:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .