Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 5 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Assigning, protecting and removing variables Python · assign_session.py
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:

declare -p for eight kinds of variable Python · declare_session.py
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

  1. Question 1 of 2 A script has the line count = 0. What happens when it runs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Bash looks for a command called count and fails with status 127

    An assignment is one word, name=value. With spaces the first word is the command name and the rest are its arguments.

  2. Question 2 of 2 declare -i total=40; total+=2. What does declare -p total print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: declare -i total="42"

    The session printed declare -i total="42" after total+=2 on the integer 40.

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:

What a child process receives Python · env_session.py
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:

Interactive · step by step Which variables a child process receives Step through the session and watch which variables are copied into each new process and which stay behind.
Which variables a child process receives, step 5 of 5. Showing plain=kept_here (shell only), SHARED=passed_on (exported), bash -c, first child (plain empty, SHARED set), bash -c, second child (GREETING set), GREETING=namaste (never in the shell). In focus: GREETING=namaste.copiedcopiedfor this commandplain=kept_hereshell onlySHARED=passed_onexportedbash -c, first childplain empty, SHARED setbash -c, second childGREETING setGREETING=namastenever in the shell

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. One command only. GREETING=namaste bash -c … puts GREETING into the environment of that one command. The child printed GREETING=[namaste].
  5. 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

  1. Question 1 of 2 You run token=abc, then python3 app.py, and the program finds no token in its environment. What fixes it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: export token before running the program, or run token=abc python3 app.py

    plain=kept_here never reached bash -c in the session, while the exported SHARED did.

  2. Question 2 of 2 After GREETING=namaste bash -c 'echo "$GREETING"', what does echo "${GREETING-unset}" print in the shell?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: unset

    The session printed after: [unset] for the same line.

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.

  1. Question 1 of 6 file=report. Which line prints report_final.pdf?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: echo "${file}_final.pdf"

    A name ends at the first character that cannot be part of a name, and _ can, so ${file} needs its braces here.

  2. Question 2 of 6 empty='' and missing was never set. What does printf "[%s] [%s]" "${empty-x}" "${missing-x}" print?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: [] [x]

    The session printed [] [unset] for the same pair with the word unset.

  3. Question 3 of 6 readonly LIMIT=10, then LIMIT=11 || echo handled. What happens in Bash 5.3.20?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: Bash prints "LIMIT: readonly variable", skips the rest of the line, and LIMIT stays 10

    Assigning to a read-only variable is an error, and in Bash 5.3.20 it ended the line it was on.

  4. Question 4 of 6 declare -l code; code=AbC. What does echo "$code" print?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: abc

    declare -p code showed declare -l code="abc" in the session.

  5. Question 5 of 6 export MODE=test, then bash -c 'MODE=live'. What is $MODE in the shell afterwards?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: test

    The environment is copied one way, from parent to child, when the child starts.

  6. Question 6 of 6 A script stores a folder in PATH=reports/2026, and every later command fails with command not found. What is the best fix?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Rename the variable to a lower-case name such as report_dir

    In the session, ( PATH=reports/2026; ls ) failed with bash: ls: command not found and status 127.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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=value has no spaces around =; with spaces Bash runs a command called name, and ${name-word} tells an unset variable from an empty one.
  • Attributes set with declare change how assignments behave, and declare -p shows 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 command sets a variable for that one command.
  • Lower-case names for script variables keep them apart from capitalised ones such as PATH and HOME, 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.