Understanding Unix shells and environment variables

Unix Insider –

A shell variable is a memory storage area that can be used to hold a value, which can then be used by any built-in shell command within a single shell. An environment variable is a shell variable that has been exported or published to the environment by a shell command so that shells and shell scripts executed below the parent shell also have access to the variable.

One built-in shell command can set a shell variable value, while another can pick it up. In the following

<font face="Courier">doecho</font>
script example,
<font face="Courier">$PLACE</font>
is set in the first line and picked up in the second line by the built-in
<font face="Courier">echo</font>
command.

Create this script and save it as

<font face="Courier">doecho</font>
. Change the mode using
<font face="Courier">chmod a+x doecho</font>
:

<font face="Courier"># doecho sample variable
PLACE=Hollywood
echo "doecho says Hello " $PLACE
</font>

Run the program as shown below.

In all of the following examples, I use the convention of

<font face="Courier">./command</font>
to execute a shell script in the current directory. You don't need to do this if your
<font face="Courier">$PATH</font>
variable contains the
<font face="Courier">.</font>
as one of the searched directories. The
<font face="Courier">./command</font>
method works for scripts in your current directory, even if the current directory isn't included on your path.

<font face="Courier">$ ./doecho
doecho says Hello Hollywood
$
</font>

In this first example,

<font face="Courier">$PLACE</font>
is a shell variable.

Now, create another shell script called

<font face="Courier">echoplace</font>
and change its mode to executable.

<font face="Courier"># echoplace echo $PLACE variable
echo "echoplace says Hello " $PLACE
</font>

Modify

<font face="Courier">doecho</font>
to execute
<font face="Courier">echoplace</font>
as its last step.

<font face="Courier"># doecho sample variable
PLACE=Hollywood
echo "doecho says Hello " $PLACE
./echoplace
</font>

Run the

<font face="Courier">doecho</font>
script. The output is a bit surprising.

<font face="Courier">$ ./doecho
doecho says Hello Hollywood
echoplace says Hello
$
</font>

In this example,

<font face="Courier">echoplace</font>
is run as the last command of
<font face="Courier">doecho</font>
. It tries to echo the
<font face="Courier">$PLACE</font>
variable but comes up blank. Say goodbye to Hollywood.

To understand what happened here you need understand something about shell invocation -- the sequence of events that occur when you run a shell or shell script. When a shell begins to execute any command, it checks to see if the command is built-in (like

<font face="Courier">echo</font>
), an executable program (like vi or
<font face="Courier">grep</font>
), a user-defined function, or an executable shell script. If it's any of the first three, it directly executes the command, function, or program; but if the command is an executable shell script, the shell spawns another running copy of itself -- a child shell. The spawned child shell uses the shell script as an input file and reads it in line by line as commands to execute.

When you type

<font face="Courier">./doecho</font>
to execute the
<font face="Courier">doecho</font>
script, you're actually executing a command that is something like one of the following, depending on which shell you're using.

<font face="Courier">$ sh < ./doecho
            (or)
$ ksh <./doecho
</font>

The new shell, spawned as a child of your starting-level shell, opens

<font face="Courier">doecho</font>
and begins reading commands from that file. It performs the same test on each command, looking for built-in commands, functions, programs, or shell scripts. Each time a shell script is encountered, another copy of the shell is spawned.

I have repeated the running of

<font face="Courier">doecho</font>
so you can follow it through the steps described below. The output of
<font face="Courier">doecho</font>
is repeated here, with extra spacing and notes.

<font face="Courier">$ ./doecho                  <-the command typed in shell one
                              launches shell two reading doecho.
doecho says Hello Hollywood <-shell two sets $PLACE and echoes
                              Shell three starts echoplace
echoplace says Hello        <-shell three cannot find $PLACE and
                              echoes a blank
$                           <-shells three and two exit. Back at shell one
</font>

As you're looking at a prompt on the screen, you're actually running a top-level shell. If you've just logged on, this will be shell one, where you type the command

<font face="Courier">./doecho</font>
. Shell two is started as a child of shell one. Its job is to read and execute
<font face="Courier">doecho</font>
. The
<font face="Courier">doecho</font>
script is repeated below.

The first command in

<font face="Courier">doecho</font>
creates the shell variable
<font face="Courier">$PLACE</font>
and assigns the value "Hollywood" to it. At this point, the
<font face="Courier">$PLACE</font>
variable only exists with this assignment inside shell two. The
<font face="Courier">echo</font>
command on the next line will print out
<font face="Courier">doecho says Hello Hollywood</font>
and move on to the last line. Shell two reads in the line containing
<font face="Courier">./echoplace</font>
and recognizes this as a shell script. Shell two launches shell three as a child process, and shell three begins reading the commands in
<font face="Courier">echoplace</font>
.

<font face="Courier"># doecho sample variable
PLACE=Hollywood
echo "doecho says Hello " $PLACE
./echoplace
</font>

The

<font face="Courier">echoplace</font>
shell script is repeated below. The only executable line in
<font face="Courier">echoplace</font>
is a repeat of the echoed message. However,
<font face="Courier">$PLACE</font>
only exists with the value "Hollywood" in shell two. Shell three sees the line to echo
<font face="Courier">echoplace says Hello</font>
and the
<font face="Courier">$PLACE</font>
variable, and cannot find any value for
<font face="Courier">$PLACE</font>
. Shell three creates its own local variable named
<font face="Courier">$PLACE</font>
as an empty variable. When it echoes the script, it's empty and prints nothing.

<font face="Courier"># echoplace echo $PLACE variable
echo "echoplace says Hello " $PLACE
</font>

The assignment of "Hollywood" to

<font face="Courier">$PLACE</font>
in shell two is only available inside shell two. If you type in a final command in shell one to echo
<font face="Courier">$PLACE</font>
at the shell one level, you'll find that
<font face="Courier">$PLACE</font>
is also blank in shell one.

<font face="Courier">$ echo "shell one says Hello " $PLACE
shell one says Hello
$
</font>

Thus far, you've only created and used a variable inside of a single shell level. You can, however, publish a shell variable to the environment, thereby creating an environment variable that's available both to the shell that published it and to all child shells started by the publishing shell. Use

<font face="Courier">export</font>
in the Bourne and Korn shells.

<font face="Courier">$ PLACE=Hollywood; export PLACE
$
</font>

The Korn shell also has a command that both exports the variable and assigns a value to it.

<font face="Courier">$ export PLACE=Hollywood
$
</font>

The C shell uses a very different syntax for shell and environment variables. Assign a value to a shell variable by using

<font face="Courier">set</font>
, then assign an environment variable using
<font face="Courier">setenv</font>
. Note that
<font face="Courier">setenv</font>
doesn't use the
<font face="Courier">=</font>
operator.

<font face="Courier">> set PLACE=Hollywood
> setenv PLACE Hollywood
</font>

Back in the Korn or Bourne shells, if we revisit the

<font face="Courier">doecho</font>
script and edit it to export the
<font face="Courier">$PLACE</font>
variable, it becomes available in shell two (the publishing shell) and shell three (the child shell).

<font face="Courier"># doecho sample variable
PLACE=Hollywood; export PLACE
echo "doecho says Hello " $PLACE
./echoplace
</font>

When

<font face="Courier">doecho</font>
is run, the output is changed. This happens because in shell three
<font face="Courier">$PLACE</font>
is found as an environment variable that has been exported from shell two.

<font face="Courier">$ ./doecho
doecho says Hello Hollywood
echoplace says Hello Hollywood
$
</font>

Assigning a value to

<font face="Courier">$PLACE</font>
before you run
<font face="Courier">doecho</font>
will help you verify its scope. After
<font face="Courier">doecho</font>
is complete, echo the value of
<font face="Courier">$PLACE</font>
at the shell one level. Notice that
<font face="Courier">doecho</font>
in shell two and
<font face="Courier">echoplace</font>
in shell three both see
<font face="Courier">$PLACE</font>
's value as "Hollywood", but the top-level shell sees the value "Burbank". This is because
<font face="Courier">$PLACE</font>
was exported in shell two. The environment variable
<font face="Courier">$PLACE</font>
has scope in shell two and shell three, but not in shell one. Shell one creates its own local shell variable named
<font face="Courier">$PLACE</font>
that is unaffected by shells two and three.

<font face="Courier">$ PLACE=Burbank
$ ./doecho
doecho says Hello Hollywood
echoplace says Hello Hollywood
$ echo "shell one says Hello " $PLACE
$ shell one says Hello Burbank
$
</font>

Once a shell variable has been exported and become an environment variable, it can be modified by a subshell. The modification affects the environment variable at all levels where the environment variable has scope.

Make some changes to

<font face="Courier">doecho</font>
by adding a repeat of the
<font face="Courier">echo</font>
line after the return from
<font face="Courier">echoplace</font>
.

<font face="Courier"># doecho sample variable
PLACE=Hollywood
echo "doecho says Hello " $PLACE
./echoplace
echo "doecho says Hello " $PLACE
</font>

After it has been echoed, modify

<font face="Courier">echoplace</font>
to change the value of
<font face="Courier">$PLACE</font>
. Once this is done, echo it again.

<font face="Courier"># echoplace echo $PLACE variable
echo "echoplace says Hello " $PLACE
PLACE=Pasadena
echo "echoplace says Hello " $PLACE
</font>

Retype the previous sequence of commands as shown below. Shell three alters the value of

<font face="Courier">$PLACE</font>
, a change that appears in shell three -- and in shell two, even after it returns from
<font face="Courier">echoplace</font>
. Once a variable is published to the environment, it's fair game to any shell at or below the publishing level.

<font face="Courier">$ PLACE=Burbank
$ ./doecho
doecho says Hello Hollywood
echoplace says Hello Hollywood
echoplace says Hello Pasadena
doecho says Hello Pasadena
$ echo "shell one says Hello " $PLACE
$ shell one says Hello Burbank
$
</font>

You have seen that the default action of a shell is to spawn a child shell whenever a shell script is encountered on the command line. Such behavior can be suppressed by using the dot command, which is a dot and a space placed before a command.

Execute

<font face="Courier">doecho</font>
by starting it with a dot and a space, then echo the value of
<font face="Courier">$PLACE</font>
when
<font face="Courier">doecho</font>
is complete. In this example, shell one recognizes
<font face="Courier">$PLACE</font>
as having been given the value "Pasadena".

<font face="Courier">$ . ./doecho
doecho says Hello Hollywood
echoplace says Hello Hollywood
echoplace says Hello Pasadena
doecho says Hello Pasadena
$ echo "shell one says Hello " $PLACE
$ shell one says Hello Pasadena
$
</font>

Normally, when a shell discovers that the command to execute is a shell script, it would spawn a child shell and that child would read in the script as commands. If the shell script is preceded by a dot and a space, however, the shell stops reading the current script or commands and starts reading in the new script or commands without starting a new subshell.

When you type in

<font face="Courier">. ./doecho</font>
, shell one doesn't spawn a child shell, but instead switches gears and begins reading from
<font face="Courier">doecho</font>
. The
<font face="Courier">doecho</font>
script initializes and exports the
<font face="Courier">$PLACE</font>
variable. The export of
<font face="Courier">$PLACE</font>
now affects all shells because you exported it at the shell one level.

A dot script is very useful for setting up a temporary environment that you don't want to set up in your

<font face="Courier">.profile</font>
. Suppose for instance that you have a specialized task that you do only on certain days, and that you need to set up some special environment variables for it. Place these variables in a file named
<font face="Courier">specvars</font>
.

<font face="Courier"># specvars contains special variables for the
# special task that I do sometimes
WORKDIR=/some/dir
SPECIALVAR="Bandy Legs"
REPETITIONS=52
export WORKDIR SPECIALVAR REPETITIONS
</font>

If you execute this variable by simply typing in the name of the

<font face="Courier">specvars</font>
file, you won't get the expected effect because a subshell, shell two, is created to execute
<font face="Courier">specvars</font>
and the
<font face="Courier">export</font>
command exports to shell two and below. Shell one doesn't view these exports as environment variables.

<font face="Courier">$ specvars
$ echo "WORKDIR IS " $WORKDIR
WORKDIR is
$ 
</font>

Using the dot command causes the script to execute as part of shell one; the effect is now correct.

<font face="Courier">
$ . specvars
$ echo "WORKDIR IS " $WORKDIR
WORKDIR is /some/dir
$ 
</font>

So there you have some of the ins and outs of shell and environment variables, as well as some ways to get around some of their limitations. If you want to see your current environment variables, type the

<font face="Courier">printenv</font>
command; a list of all variables that are available to the current shell, including all child shells, is printed out.

What’s wrong? The new clean desk test
Join the discussion
Be the first to comment on this article. Our Commenting Policies