Bash & the POSIX Shell Module 3 – Variables, parameters and arithmetic
Variables, export and the environment
Assign, read, protect and remove Bash variables, see which ones a child process receives, set one for a single command and inspect them with declare -p.
What you will learn
- Set, read, unset and protect variables, and distinguish an unset variable from an empty one
- Explain the difference between shell variables and exported environment variables
- Set a variable for one command only and inspect variables with declare -p
Before you start
What you’ll learn · Before you start
What you will learn
- Set, read, unset and protect variables, and distinguish an unset variable from an empty one
- Explain the difference between shell variables and exported environment variables
- Set a variable for one command only and inspect variables with declare -p
Before you start
On this page
city = Pune looks like an assignment with spaces for readability, yet Bash answers “city: command not found” and
the command fails with status 127. To Bash, a line that starts with a word followed by a space is a command, so the spaces turned
an assignment into a call of a program named city.
Here you assign, read, protect and remove variables, give them attributes and inspect them with declare -p, see
exactly which variables a program started from your shell receives, and set one for a single command.
An assignment has no spaces, and an unset variable differs from an empty one
A shell variable: A name that holds a value inside the shell, set with name=value and read with $name. Bash keeps every value as a string unless the variable has an attribute.
is created by name=value, with nothing around the =. Its value is read with $name, or ${name} when letters
follow the name, and unset name removes it. The session assigns, reads, tries the spaced form, protects one value
and removes another:
from session import run
# Assigning, reading, protecting and removing variables. A space around = turns the line into a command; a readonly
# variable refuses a new value; ${name-unset} tells an unset variable from an empty one.
run("""
city=Pune
echo "$city"
!city = Pune
empty=''
printf '[%s] [%s]\\n' "${empty-unset}" "${missing-unset}"
readonly LIMIT=10
!LIMIT=11 || echo handled
echo "LIMIT is still $LIMIT"
unset city
printf '[%s]\\n' "${city-unset}"
""") Output
$ city=Pune
$ echo "$city"
Pune
$ city = Pune
bash: city: command not found
[exit status 127]
$ empty=''
$ printf '[%s] [%s]\n' "${empty-unset}" "${missing-unset}"
[] [unset]
$ readonly LIMIT=10
$ LIMIT=11 || echo handled
bash: LIMIT: readonly variable
[exit status 1]
$ echo "LIMIT is still $LIMIT"
LIMIT is still 10
$ unset city
$ printf '[%s]\n' "${city-unset}"
[unset]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assign_session.py
city=Pune and echo "$city" printed Pune, while city = Pune ran a command called city and failed with status
127. Two variables that print the same can still differ: empty='' is set to the empty string, and missing was never
set. ${name-word} gives word only when name is unset, so "${empty-unset}" printed nothing between its
brackets and "${missing-unset}" printed unset. After unset city, ${city-unset} gave unset too.
readonly LIMIT=10 made the variable unchangeable. LIMIT=11 failed with “LIMIT: readonly variable”, and Bash 5.3.20
abandoned the rest of that line, so even the || echo handled after it never ran; the next line ran normally and
showed LIMIT still at 10.
| You write | Bash does |
|---|---|
name=value |
sets the variable, for the rest of the shell’s life |
name = value |
runs a command called name with two arguments |
unset name |
removes the variable; ${name-x} then gives x |
readonly name=value |
sets it and refuses every later change |
Spaces around the equals sign
Wrong: total = 0, which looks for a program called total. Right: total=0. A value with spaces needs quotes
instead, title='Monthly report', or the second word is taken as a command.
Attributes change how Bash stores a variable, and declare shows them
Every Bash variable holds a string unless it has an attribute. declare sets attributes: -i makes a variable an
integer, so assignments to it are evaluated as arithmetic; -a and -A make indexed and associative arrays; -r
makes it read-only; -x exports it; -l and -u convert the value to lower or upper case on every assignment.
declare -p prints a variable as the command that would recreate it, attributes included, which makes it the most
exact way to inspect one:
from session import run
# declare -p shows how Bash stores each variable: its attributes and its value, as a command that recreates it.
run("""
title='Monthly report'
declare -i total=40
total+=2
names=(Asha 'Ravi Kumar')
declare -A ages=([asha]=31)
declare -r MAX_USERS=50
declare -x EDITOR=nano
declare -l code=AbC
declare -u tag=new
declare -p title total names ages MAX_USERS EDITOR code tag
""") Output
$ title='Monthly report' $ declare -i total=40 $ total+=2 $ names=(Asha 'Ravi Kumar') $ declare -A ages=([asha]=31) $ declare -r MAX_USERS=50 $ declare -x EDITOR=nano $ declare -l code=AbC $ declare -u tag=new $ declare -p title total names ages MAX_USERS EDITOR code tag declare -- title="Monthly report" declare -i total="42" declare -a names=([0]="Asha" [1]="Ravi Kumar") declare -A ages=([asha]="31" ) declare -r MAX_USERS="50" declare -x EDITOR="nano" declare -l code="abc" declare -u tag="NEW"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 declare_session.py
Each line of the output starts with declare and the attributes. title shows --, no attributes: a plain string.
total was declared with -i, so total+=2 added 2 to 40 instead of appending the text, and it holds "42".
names and ages show their elements, [1]="Ravi Kumar" kept whole. code was assigned AbC but holds abc,
and tag holds NEW, because -l and -u act on every assignment. The quotes around the values show that Bash still
stores the integer as text; the attribute only changes what an assignment does.
Inspect with declare -p, not echo
echo "$x" shows the value only. declare -p x shows the type and the attributes too, and for an array every element
with its index, so an array mistaken for a string, or a value with a trailing space, is visible at once.
The -x in that list matters for the next section: it is the attribute export sets, so declare -p shows an x
among the attributes of every variable a child process will receive.
Key idea
A variable is a string until an attribute says otherwise, and declare -p shows both the value and the attributes in one line.
Checkpoint
Check: Assigning, unset and empty · Attributes and declare -p
Only exported variables reach the programs you start
Every program receives a list of name and value pairs from the process that started it: its
environment variable: A variable that has been exported, so every program the shell starts receives a copy of it in its environment, the list of name=value pairs a process begins with..
A shell variable is not in that list until it is exported, with export name or declare -x name. A child process
gets a copy, so nothing it changes ever comes back to the shell:
from session import run
# A child process sees only exported variables. NAME=value before a command exports NAME to that command alone, and a
# script variable that reuses a name the shell depends on, such as PATH, breaks every command after it.
run("""
plain=kept_here
export SHARED=passed_on
bash -c 'printf "plain=[%s] SHARED=[%s]\\n" "$plain" "$SHARED"'
GREETING=namaste bash -c 'printf "GREETING=[%s]\\n" "$GREETING"'
printf 'after: [%s]\\n' "${GREETING-unset}"
env | grep -E '^(SHARED|plain)='
dash -c 'printf "dash sees SHARED=[%s]\\n" "$SHARED"'
!( PATH=reports/2026; ls )
""") Output
$ plain=kept_here
$ export SHARED=passed_on
$ bash -c 'printf "plain=[%s] SHARED=[%s]\n" "$plain" "$SHARED"'
plain=[] SHARED=[passed_on]
$ GREETING=namaste bash -c 'printf "GREETING=[%s]\n" "$GREETING"'
GREETING=[namaste]
$ printf 'after: [%s]\n' "${GREETING-unset}"
after: [unset]
$ env | grep -E '^(SHARED|plain)='
SHARED=passed_on
$ dash -c 'printf "dash sees SHARED=[%s]\n" "$SHARED"'
dash sees SHARED=[passed_on]
$ ( PATH=reports/2026; ls )
bash: ls: command not found
[exit status 127]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 env_session.py
The child bash -c printed plain=[] and SHARED=[passed_on]: the exported variable arrived and the plain one did
not. env, which prints its own environment, listed SHARED and not plain, and dash, a different shell, received
SHARED in the same way, because the environment belongs to the operating system’s way of starting programs, not to
Bash. Step through the session:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A plain variable. plain=kept_here makes a shell variable. It lives in this shell only, and no program started from here can see it.
- An exported variable. export SHARED=passed_on marks the variable for export, so it becomes part of the environment that every new process gets a copy of.
- The first child starts. bash -c starts a new process with a copy of the environment. It printed plain=[] SHARED=[passed_on]: the exported value arrived, the plain one did not.
- One command only. GREETING=namaste bash -c … puts GREETING into the environment of that one command. The child printed GREETING=[namaste].
- Gone afterwards. Back in the shell, ${GREETING-unset} gave unset: the assignment belonged to the command, and the shell itself never had the variable.
GREETING=namaste bash -c … put GREETING into the environment of that command only: the child printed it, and
afterwards ${GREETING-unset} in the shell gave unset. The form is handy for a setting that one command needs,
such as LC_ALL=C sort file or TZ=UTC date, without changing the shell.
The last command shows why names matter. A script that keeps a folder name in a variable called PATH replaces the
list of folders where Bash looks for programs, and ls then failed with “command not found”, status 127. Exported
variables and the shell’s own variables are written in capitals by convention, so give script variables lower-case
names, report_dir rather than PATH or HOME, and they can never collide with one the shell or another program
depends on.
Expecting a child to change the parent
Wrong: running bash setup.sh and expecting the variables it sets to exist afterwards. Right: a child’s changes die
with it; run the file in the current shell with source setup.sh, or have it print values for the caller to read.
Checkpoint
Check: What a child process receives
Practise: variables and what children see
The questions show assignments, attributes and child processes like the ones in the sessions. Decide what each line prints, then check your answer against the recorded output:
Practice that adapts to you
Practise this lesson
6 questions about 3 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): how is a shell variable different from an environment variable?
A shell variable exists only inside the shell that set it: name=value creates one, and $name reads it. An
environment variable is one that has been exported, with export name or declare -x, so that every program the shell
starts receives a copy of it in its environment. The copy goes one way only: a child process can change its copy, but
the change never reaches the parent shell. To give a variable to a single command without exporting it, write it
before the command, as in TZ=UTC date. env lists the environment, and declare -p name shows whether a variable
carries the -x attribute.
Two harder questions follow, about a program that cannot see a variable and about a script whose commands stop being found. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A script sets API_URL=https://api.example.test and then runs python3 report.py, which reads the variable from its environment and stops with KeyError: API_URL. Explain the cause and two ways to fix it.
-
Mid to senior
Halfway through a long script every command starts failing with command not found, although the same commands work in a terminal. The line before the failures is PATH=$report_dir. What happened, and how do you prevent this class of bug?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why does city = Pune fail in Bash?
With spaces around the equals sign it is no longer an assignment: Bash runs a command named city with the arguments = and Pune, and answers city: command not found. Write city=Pune.
-
How can a script tell an unset variable from an empty one?
With ${name-word}, which gives word only when name is unset and keeps an empty value as it is. ${name:-word} treats unset and empty alike.
-
What does declare -p name print?
The variable as the declare command that would recreate it, attributes included, which makes it the clearest way to see what a variable is and holds.
-
Which variables does a program started from the shell receive?
Only exported ones, set with export or declare -x. The child gets a copy, so a change it makes never travels back to the parent shell.
-
How do you give one command a variable without changing the shell?
Put the assignment in front of it, as in LC_ALL=C sort file. The variable is in that command's environment only, and the shell's own value stays as it was.
-
Why should a script name its own variables in lower case?
Capitalised names such as PATH and HOME belong to the shell and other programs. Overwriting PATH by accident makes every later program fail with command not found, status 127.
Key takeaways
name=valuehas no spaces around=; with spaces Bash runs a command calledname, and${name-word}tells an unset variable from an empty one.- Attributes set with
declarechange how assignments behave, anddeclare -pshows a variable’s attributes and value as the command that recreates it. - Only exported variables are copied into the environment of a child process, a child’s changes never return, and
NAME=value commandsets a variable for that one command. - Lower-case names for script variables keep them apart from capitalised ones such as
PATHandHOME, which the shell and other programs depend on.
Terms in this lesson
- environment variable
- A variable that has been exported, so every program the shell starts receives a copy of it in its environment, the list of name=value pairs a process begins with.
- shell variable
- A name that holds a value inside the shell, set with name=value and read with $name. Bash keeps every value as a string unless the variable has an attribute.
References
- Bash Reference Manual: Shell Parameters (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Environment (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Bash Builtin Commands (declare) (Chet Ramey (GNU Bash maintainer))
- POSIX.1-2024: Shell Command Language (2.5.3 Shell Variables) (The Open Group and IEEE)
- POSIX.1-2024: Base Definitions, 8.1 Environment Variable Definition (The Open Group and IEEE)
Report a problem with this lesson
Kept only in this browser. Your Learn progress