What is the difference in usage between shell variables and environment variables?

Shell variables

Shell variables are variables whose scope is in the current shell session, for example in an interactive shell session or a script.

You may create a shell variable by assigning a value to an unused name:

var="hello"

The use of shell variables is to keep track of data in the current session. Shell variables usually have names with lower-case letters.

Environment variables

An environment variable is a shell variable which has been exported. This means that it will be visible as a variable, not only in the shell session that created it, but also for any process (not just shells) that are started from that session.

VAR="hello"  # shell variable created
export VAR   # variable now part of the environment

or

export VAR="hello"

Once a shell variable has been exported, it stays exported until it is unset, or until its "export property" is removed (with export -n in bash), so there's usually no need to re-export it. Unsetting a variable with unset deletes it (no matter if it's an environment variable or not).

Arrays and associative hashes in bash and other shells may not be exported to become environment variables. Environment variables must be simple variables whose values are strings, and they often have names consisting of upper-case letters.

The use of environment variables is to keep track of data in the current shell session, but also to allow any started process to take part of that data. The typical case of this is the PATH environment variable, which may be set in the shell and later used by any program that wants to start programs without specifying a full path to them.

The collection of environment variables in a process is often referred to as "the environment of the process". Each process has its own environment.

Environment variables can only be "forwarded", i.e. a child process can never change the environment variables in its parent process, and other than setting up the environment for a child process upon starting it, a parent process may not change the existing environment of a child process.

Environment variables may be listed with env (without any arguments). Other than that, they appear the same as non-exported shell variables in a shell session. This is a bit special for the shell as most other programming languages don't usually intermix "ordinary" variables with environment variables (see below).

env may also be used to set the values of one or several environment variables in the environment of a process without setting them in the current session:

env CC=clang CXX=clang++ make

This starts make with the environment variable CC set to the value clang and CXX set to clang++.

It may also be used to clear the environment for a process:

env -i bash

This starts bash but does not transfer the current environment to the new bash process (it will still have environment variables as it creates new ones from its shell initialization scripts).

Example of difference

$ var="hello"   # create shell variable "var"
$ bash          # start _new_ bash session
$ echo "$var"   # no output
$ exit          # back to original shell session
$ echo "$var"   # "hello" is outputted
$ unset var     # remove variable

$ export VAR="hello"  # create environment variable "VAR"
$ bash
$ echo "$VAR"         # "hello" is outputted since it's exported
$ exit                # back to original shell session
$ unset VAR           # remove variable

$ ( export VAR="hello"; echo "$VAR" )  # set env. var "VAR" to "hello" in subshell and echo it
$ echo "$VAR"         # no output since a subshell has its own environment

Other languages

There are library functions in most programming languages that allows for getting and setting the environment variables. Note that since environment variables are stored as a simple key-value relationship, they are not usually "variables" of the language. A program may fetch the value (which is always a character string) corresponding to a key (the name of the environment variable), but will then have to convert it to an integer or whatever data type the language expects the value to have.

In C, environment variables may be accessed using getenv(), setenv(), putenv() and unsetenv(). Variables created with these routines are inherited in the same way by any process that the C program starts.

Other languages may have special data structures for accomplishing the same thing, like the %ENV hash in Perl, or the ENVIRON associative array in most implementations of awk.


Environment variables are a list of name=value pairs that exist whatever the program is (shell, application, daemon…). They are typically inherited by children processes (created by a fork/exec sequence): children processes get their own copy of the parent variables.

Shell variables do exist only in the context of a shell. They are only inherited in subshells (i.e. when the shell is forked without an exec operation). Depending on the shell features, variables might not only be simple strings like environment ones but also arrays, compound, typed variables like integer or floating point, etc.

When a shell starts, all the environment variables it inherits from its parent become also shell variables (unless they are invalid as shell variables and other corner cases like IFS which is reset by some shells) but these inherited variables are tagged as exported1. That means they will stay available for children processes with the potentially updated value set by the shell. That is also the case with variables created under the shell and tagged as exported with the export keyword.

Array and other complex type variables cannot be exported unless their name and value can be converted to the name=value pattern, or when a shell specific mechanism is in place (e.g.: bash exports functions in the environment and some exotic, non POSIX shells like rc and es can export arrays).

So the main difference between environment variables and shell variables is their scope: environment variables are global while non exported shell variables are local to the script.

Note also that modern shells (at least ksh and bash) support a third shell variables scope. Variables created in functions with the typeset keyword are local to that function (The way the function is declared enables/disables this feature under ksh, and persistence behavior is different between bash and ksh). See https://unix.stackexchange.com/a/28349/2594

1This applies to modern shells like ksh, dash, bash and similar. The legacy Bourne shell and non Bourne syntax shells like csh have different behaviors.


Shell variables are difficult to duplicate.

$ FOO=bar
$ FOO=zot
$ echo $FOO
zot
$ 

Environment variables however can be duplicated; they are just a list, and a list can have duplicate entries. Here's envdup.c to do just that.

#include <err.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

extern char **environ;

int main(int argc, char *argv[]) {
    char **newenv;
    int envcount = 0;

    if (argc < 2) errx(64, "Usage: envdup command [args ..]");

    newenv = environ;
    while (*newenv++ != NULL) envcount++;

    newenv = malloc(sizeof(char *) * (envcount + 3));
    if (newenv == NULL) err(1, "malloc failed");
    memcpy(newenv, environ, sizeof(char *) * envcount);
    newenv[envcount]   = "FOO=bar";
    newenv[envcount+1] = "FOO=zot";
    newenv[envcount+2] = NULL;

    environ = newenv;
    argv++;
    execvp(*argv, argv);
    err(1, "exec failed '%s'", *argv);
}

Which we can compile and run telling envdup to then run env to show us what environment variables are set...

$ make envdup
cc     envdup.c   -o envdup
$ unset FOO
$ ./envdup env | grep FOO
FOO=bar
FOO=zot
$ 

This is perhaps only useful for finding bugs or other oddities in how well programs handle **environ.

$ unset FOO
$ ./envdup perl -e 'exec "env"' | grep FOO
FOO=bar
$ ./envdup python3 -c 'import os;os.execvp("env",["env"])' | grep FOO
FOO=bar
FOO=zot
$ 

Looks like Python 3.6 here blindly passes along the duplicates (a leaky abstraction) while Perl 5.24 does not. How about the shells?

$ ./envdup bash -c 'echo $FOO; exec env' | egrep 'bar|zot'
zot
FOO=zot
$ ./envdup zsh -c 'echo $FOO; exec env' | egrep 'bar|zot' 
bar
FOO=bar
$ 

Gosh, what happens if sudo only sanitizes the first environment entry but then bash runs with the second? Hello PATH or LD_RUN_PATH exploit. Is your sudo (and everything else?) patched for that hole? Security exploits are neither "an anecdotal difference" nor just "a bug" in the calling program.