50

I just ran into a problem that shows me I'm not clear on the scope of shell variables.

I was trying to use bundle install, which is a Ruby command that uses the value of $GEM_HOME to do its work. I had set $GEM_HOME, but the command ignored that value until I used export, as in export GEM_HOME=/some/path.

I read that this makes the variable somehow "global" (also known as an environment variable), but I don't understand what that means. I know about globals in programming, but not across distinct programs.

Also, given that my setting such variables applies only to the current shell session, how would I set them for, say, a daemonized process?

What scopes can shell variables have?

Nathan Long
  • 1,623

5 Answers5

39

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.

enzotib
  • 51,661
  • 1
    Ah! OK, let's see if I understand this. In the shell, if I say FOO=bar, that sets the value for the current shell process. If I then run a program like (bundle install), that creates a child process, which doesn't get access to FOO. But if I had said export FOO=bar, the child process (and its descendants) would have access to it. One of them could, in turn, call export FOO=buzz to change the value for its descendants, or just FOO=buzz to change the value only for itself. Is that about right? – Nathan Long Dec 24 '11 at 12:17
  • 2
    @NathanLong That's not exactly it: in all modern shells, a variable is either exported (and so any change in value is reflected in the environment of descendents) or not exported (meaning that the variable is not in the environment). In particular, if the variable is already in the environment when the shell starts, it is exported. – Gilles 'SO- stop being evil' Dec 24 '11 at 16:02
  • 2
    I was a little confused by the sentence "if a child change the value of a variable, the changed value is only visible to it and all processes created after that change". It would be more correct to say "...visible to it and all of its descendant processes created after that change" – the other children of the parent process, even those started after the child process, are not affected. – Jaan Nov 25 '15 at 18:20
33

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 are not displayed outside the current shell and the function local variable has no value outside the function itself. The last test shows 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.

Melebius
  • 768
jlliagre
  • 61,204
16

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
Nathan Long
  • 1,623
4

There is a hierarchy of variable scopes, as expected.

Environment

The outermost scope is the environment. This is the only scope managed by the operating system and is therefore guaranteed to exist for every process. When a process is started it receives a copy of its parent's environment after which the two become independent: modifying the child's environment does not change the parent's, and modifying the parent's environment does not change that of an already existing child.

Shell variables

Shells have their own notion of variables. This is where things start to become a bit confusing.

When you assign a value to a variable in a shell, and that variable already exists in the environment, the environment variable receives the new value. However if the variable is not in the environment yet it becomes a shell variable. Shell variables only exist within the shell process, similar to how Ruby variables only exist within a Ruby script. They are never inherited by child processes.

Here's where the export keyword comes into play. It copies a shell variable into the shell process's environment making it possible for child processes to inherit.

Local variables

Local variables are shell variables scoped to the code blocks containing them. You declare local variables with the typeset keyword (portable) or local or declare (Bash). Like other shell variables local variables are not inherited by child processes. Also local variables cannot be exported.

SnakE
  • 151
3

The best explanation I can find about exporting is this one:

http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_02.html

The variable set within a subshell or child shell is only visible to the subshell in which it is defined. The exported Variable is actually made to be an environment variable. So to be clear your bundle install executes its own shell which doesn't see the $GEM_HOME unless it is made an environment variable aka exported.

You can take a look at the documentation for variable scope here:

http://www.tldp.org/LDP/abs/html/subshells.html

Karlson
  • 5,875