What scopes can shell variables have?

The processes are organized as a tree: every process has a unique parent, apart from init which PID is always 1 and has no parent.

The creation of a new process goes generally through a pair of fork/execv system calls, where the environment of the child process is a copy of the parent process.

To put a variable in the environment from the shell you have to export that variable, so that it is visible recursively to all children. But be aware that if a child change the value of a variable, the changed value is only visible to it and all processes created after that change (being a copy, as previously said).

Take also into account that a child process could change its environment, for example could reset it to default values, as is probably done from login for example.


At least under ksh and bash, variables can have three scopes, not two like all remaining answers are currently telling.

In addition to the exported (i.e. environment) variable and shell unexported variable scopes, there is also a third narrower one for function local variables.

Variables declared in shell functions with the typeset token are only visible inside the functions they are declared in and in (sub) functions called from there.

This ksh / bash code:

# Create a shell script named /tmp/show that displays the scoped variables values.    
echo 'echo [$environment] [$shell] [$local]' > /tmp/show
chmod +x /tmp/show

# Function local variable declaration
function f
{
    typeset local=three
    echo "in function":
    . /tmp/show 
}

# Global variable declaration
export environment=one

# Unexported (i.e. local) variable declaration
shell=two

# Call the function that creates a function local variable and
# display all three variable values from inside the function
f

# Display the three values from outside the function
echo "in shell":
. /tmp/show 

# Display the same values from a subshell
echo "in subshell":
/tmp/show

# Display the same values from a disconnected shell (simulated here by a clean environment start)
echo "in other shell"
env -i /tmp/show 

produces this output:

in function:
[one] [two] [three]
in shell:
[one] [two] []
in subshell:
[one] [] []
in other shell
[] [] []

As you can see, the exported variable is displayed from the first three locations, the unexported variables is not displayed outside the current shell and the function local variable has no value outside the function itself. The last test show no values at all, this is because exported variables are not shared between shells, i.e. they can only be inherited and the inherited value cannot be affected afterwards by the parent shell.

Note that this latter behavior is quite different from the Windows one where you can use System Variables which are fully global and shared by all processes.


They are scoped by process

The other answerers helped me to understand that shell variable scope is about processes and their descendants.

When you type a command like ls on the command line, you're actually forking a process to run the ls program. The new process has your shell as its parent.

Any process can have its own "local" variables, which are not passed to child processes. It can also set "environment" variables, which are. Using export creates an environment variable. In either case, unrelated processes (peers of the original) will not see the variable; we are only controlling what child processes see.

Suppose you have a bash shell, which we'll call A. You type bash, which creates a child process bash shell, which we'll call B. Anything you called export on in A will still be set in B.

Now, in B, you say FOO=b. One of two things will happen:

  • If B did not receive (from A) an environment variable called FOO, it will create a local variable. Children of B will not get it (unless B calls export).
  • If B did receive (from A) an environment variable callled FOO, it will modify it for itself and its subsequently forked children. Children of B will see the value that B assigned. However, this will not affect A at all.

Here's a quick demo.

FOO=a      # set "local" environment variable
echo $FOO  # 'a'
bash       # forks a child process for the new shell
echo $FOO  # not set
exit       # return to original shell
echo $FOO  # still 'a'

export FOO # make FOO an environment variable
bash       # fork a new "child" shell
echo $FOO  # outputs 'a'
FOO=b      # modifies environment (not local) variable
bash       # fork "grandchild" shell
echo $FOO  # outputs 'b'
exit       # back to child shell
exit       # back to original shell
echo $FOO  # outputs 'a'

All of this explains my original problem: I set GEM_HOME in my shell, but when I called bundle install, that created a child process. Because I hadn't used export, the child process didn't receive the shell's GEM_HOME.

Un-exporting

You can "un-export" a variable - prevent it from being passed to children - by using export -n FOO.

export FOO=a   # Set environment variable
bash           # fork a shell
echo $FOO      # outputs 'a'
export -n FOO  # remove environment var for children
bash           # fork a shell
echo $FOO      # Not set
exit           # back up a level
echo $FOO      # outputs 'a' - still a local variable