Variables, Quoting and Expansion · about 40 min

Variables and the environment

Why this matters

Scripts need to remember things: a file name, a count, a directory to back up, the result of a command. In the shell, that memory is the variable. Variables are also how configuration reaches programs: PATH, HOME, LANG, EDITOR and hundreds of tool-specific settings are variables in the environment that every process inherits from its parent. Knowing exactly which variables a program can see, and why, saves hours of "but I set it!" debugging.

Assigning and using a variable

$ name=Ada
$ echo $name
Ada
$ echo "Hello, $name"
Hello, Ada
$ echo "Hello, ${name}!"
Hello, Ada!

Rules for assignment:

To use a variable you write $name; bash replaces it with the value before running the command. This replacement is called parameter expansion. Braces ${name} mark exactly where the name ends, which you need when letters follow:

$ fruit=apple
$ echo "$fruits"

$ echo "${fruit}s"
apples

$fruits asked for a variable called fruits, which does not exist. An unset variable expands to nothing, with no error by default. (Module 12 shows how to make that an error.)

Change a variable by assigning again; remove it with the builtin unset:

$ name=Grace
$ echo $name
Grace
$ unset name
$ echo "[$name]"
[]

Note: Notice that unset name has no $. You are naming the variable, not using its value. The same goes for assignment and for export name. $ means "the value of".

Where variables live

A shell variable lives inside the memory of one bash process. Nothing else can see it. That is fine as long as you stay in that shell, but every external program is a separate process, as you learned with fork and exec. So how does ls know your LANG, or how does man know which pager to use?

The answer is the environment: a list of NAME=value strings that the kernel hands to every new program when it is exec'd. Every process has one. When bash forks a child and execs a program, it builds the environment from the variables that are marked for export.

   bash process memory
   ┌──────────────────────────────────────┐
   │ shell variables:  city=Paris         │   not exported: stays here
   │                   count=3            │
   │ exported:         HOME=/home/learner │──┐
   │                   PATH=/usr/bin:...  │  │  copied into the environment
   │                   country=France     │  │  of every program bash starts
   └──────────────────────────────────────┘  │
                                             ▼
                     execve("/usr/bin/env", args, ["HOME=...", "PATH=...", "country=France"])

So there are two kinds of variable:

export marks a variable; env (or printenv) prints the environment that a child would see:

$ city=Paris
$ export country=France
$ env | grep -e city -e country
country=France
$ printenv country
France
$ printenv city
$ bash -c 'echo "city=$city country=$country"'
city= country=France

bash -c 'some code' starts a new bash process that runs the code. It is a handy way to see what a child receives. The single quotes matter here: they stop your shell from expanding the variables, so the child does the expanding with what it was given.

The variables you start with (HOME, PATH, LANG, TERM) are already exported: your shell inherited them from whatever started it (here, the container runtime) and passes them on.

Copies, not shared memory

The child receives a copy. If it changes or exports variables, those changes die with it. There is no way for a child process to change its parent's variables:

$ export mood=calm
$ bash -c 'mood=angry; echo "child: $mood"'
child: angry
$ echo "parent: $mood"
parent: calm

This is the same reason as cd being a builtin, and it is why a script that sets variables does not change your interactive shell when you run it as a program. (You will learn source, which runs a script inside the current shell, in the next module.)

Setting a variable for one command

You can put assignments in front of a command. They go into that command's environment only, and the shell's own variables are untouched:

$ LANG=C date
Sun Nov  1 14:03:12 UTC 2026
$ TZ=America/New_York date
Sun Nov  1 09:03:12 EST 2026
$ echo "[$TZ]"
[]

TZ tells time functions which time zone to use. This prefix form is common in scripts: LC_ALL=C sort file to sort by raw bytes, EDITOR=nano crontab -e, and so on.

Special and well-known variables

Some variables are set and used by bash itself:

VariableMeaning
HOMEyour home directory; cd with no argument goes there
PATHwhere to look for commands (lesson sh-01-02)
PWD, OLDPWDcurrent and previous directory (cd - uses OLDPWD)
PS1the prompt string
SHELLyour login shell (not necessarily the one running)
RANDOMa new random number 0-32767 each time you read it
$?, $$last exit status, PID of the shell (special parameters, no name)

Try echo $RANDOM $RANDOM and echo $PWD.

A naming convention keeps you out of trouble: environment and bash's own variables are UPPERCASE, so use lowercase for your own script variables. If you write PATH=/backup in a script meaning "the backup path", every command after that is "not found".

set with no arguments prints every shell variable and function (a lot of output; pipe it into less). declare -p name prints one variable in a form you could paste back:

$ declare -p country
declare -x country="France"

The -x means exported.

Common mistakes

Exercises

The quizzes test the fork/exec model of the environment, which you will lean on in every script you write. The task uses your lesson shell; the code exercise runs as bash script.sh.

Recap

Going further: run env | sort | less and look up two variables you do not recognize. Then look at /proc/self/environ: tr '\0' '\n' < /proc/self/environ. That file is the environment the kernel gave to the tr process itself, NUL-separated exactly as execve received it.

Exercises

Exercise 1 · quiz

An assignment that is not one

You type name = Ada and bash answers bash: name: command not found. Why?

Answer
  • Because of the spaces, bash splits the line into three words and runs name as a command with the arguments = and Ada.

An assignment is only recognized when the whole word looks like name=value with no unquoted spaces around the =. With spaces, word splitting produces three words and the first one is taken as a command name. The same happens with name =Ada (command name, argument =Ada) and name= Ada which is subtler: it runs the command Ada with name set to the empty string in its environment.

Exercise 2 · quiz

What does the child see?

In a fresh shell you run these lines, one after another:

city=Paris
export country=France
bash -c 'echo "$city/$country"'

What does the last line print?

Answer
  • /France

bash -c starts a new bash process (fork, then exec). Only exported variables are copied into the environment that execve hands to the new program, so the child knows country but has never heard of city; an unknown variable expands to nothing. The single quotes stop your shell from expanding $city itself, so the child shell is the one that expands it. Change the quotes to double quotes and your shell expands both before the child starts: then it prints Paris/France.

Exercise 3 · task in a shell

Read the environment

In the shell:

  1. Save the value of the HOME variable into /work/home.txt (one line).
  2. Save the value of the environment variable LANG into /work/lang.txt using printenv.
  3. Create a shell variable course with the value bash basics, then save the line I am learning bash basics into /work/course.txt by expanding $course (do not type bash basics a second time).
Hint 1

echo "$HOME" prints the value. printenv LANG prints one environment variable.

Hint 2

course='bash basics' (no spaces around =), then echo "I am learning $course" > course.txt.

Reference solution
echo "$HOME" > home.txt
printenv LANG > lang.txt
course='bash basics'
echo "I am learning $course" > course.txt

Exercise 4 · write code

Pass it to the child

The script sets a variable and then starts a child bash that prints it. As written, the child prints an empty line, because it never receives the variable.

Fix the script so that it prints exactly:

greeting from the parent: hello

Keep the bash -c '...' line as it is (with its single quotes); change only how the variable is set up.

File: script.sh

Starting point

GREETING=hello
bash -c 'echo "greeting from the parent: $GREETING"'

Example

expected output

greeting from the parent: hello
Hint 1

A child process only receives variables that are in the environment.

Hint 2

export GREETING=hello, or GREETING=hello followed by export GREETING.

Reference solution
export GREETING=hello
bash -c 'echo "greeting from the parent: $GREETING"'