Linux & Bash · Part I · Module 8 · Variables, Command Substitution and Arithmetic
Creating and Using a Shell Variable
A variable is a name you give to a piece of text so you can use it again. The idea takes thirty seconds; the two rules that come with it take rather longer, and skipping either produces an error message that does not obviously point back at what you typed.
Foundation17 min readLinuxPart IFundamentalsVariablesBash
Part I · Module 8 · Chapter 8.1 · Creating and Using a Shell Variable
Everything so far has been commands acting on things that already exist. This module is about making the shell hold something — a value you computed once and want to use three more times — which is the step between using a shell and programming one.
1. Storing and Reading
$ name=run_42
$ echo "$name"
run_42Two halves, and they use different syntax, which is the first thing to fix in your head:
- To store, write the name, an equals sign, and the value:
name=run_42. No dollar sign. - To read, write a dollar sign in front of the name:
$name. Always a dollar sign.
The asymmetry is not arbitrary. On the left of an = the shell wants a name; everywhere else, a bare word is just a word, and $ is what tells the shell to replace it with a value.
$ name=run_42
$ echo name
name
$ echo $name
run_42The first echo printed the word name, because that is what you typed. Only the dollar sign makes it a variable reference.
2. The Spaces Rule
This is the single most common error in the whole of Bash, and it is worth meeting all three versions of it, because they produce three different messages.
$ name=run_42
$ echo "$name"
run_42That works. Now add a space.
$ name = run_42
bash: name: command not found$ name =run_42
bash: name: command not found$ name= run_42
bash: run_42: command not foundNo spaces are allowed around the = in an assignment. The reason is that the shell decides what a line is by looking at the first word. With a space, name is no longer part of an assignment — it is the first word of a command line, so the shell goes looking for a program called name, and there isn't one.
3. Everything Is Text
The second rule surprises people coming from other languages.
$ n=5
$ echo "$n+1"
5+1
$ echo $n+1
5+15+1, not 6. A shell variable holds a string. n=5 stores the one-character text 5, and $n+1 produces the three-character text 5+1, because nothing in that line asked for arithmetic.
There is no "number" type to declare and nothing is being coerced. Arithmetic requires asking for it explicitly, which is Chapter 8.5:
$ echo "$(( n + 1 ))"
6This is not a flaw. The shell's job is to assemble command lines out of text, so text is the only type it needs — and a value that looks like a number is only ever a number because some command chose to read it that way.
4. Expansion Happens Before the Command Runs
Here is the model that makes the rest of this module — and the whole of Module 9 — make sense.
When you press Enter, the shell does not hand your line to the command. It processes the line first: it replaces every $name with that variable's value, decides how the result breaks into separate arguments, and only then starts the program.
So the command never sees $name. By the time it exists, there is no variable left — only the text that replaced it.
$ f="my report.txt"
$ ls $f
ls: my: No such file or directory
ls: report.txt: No such file or directoryRead the two errors carefully. ls did not complain about a file called my report.txt. It complained about two files, my and report.txt, because that is genuinely what it was given. The shell replaced $f with my report.txt and then split that text at the space into two separate arguments.
Quotes prevent the splitting:
$ ls -l "$f"
-rw-r--r-- 1 engineer engineer 0 Mar 28 12:49 my report.txt5. Naming
$ ok_1=x
$ echo "$ok_1"
x
$ 2bad=x
bash: 2bad=x: command not found
$ my-var=x
bash: my-var=x: command not foundA variable name may contain letters, digits and underscores, and may not start with a digit. Nothing else — no hyphens, no dots, no spaces. A name the shell does not recognise as a name means the line is not an assignment, which is why both failures report command not found rather than anything about naming.
Names are case-sensitive, and name and NAME are two different variables:
$ name=lower
$ NAME=UPPER
$ echo "[$name] [$NAME]"
[lower] [UPPER]6. Unset, Empty, and the Difference
A variable that was never set and a variable set to nothing both expand to nothing:
$ empty=""
$ echo "empty=[$empty] missing=[$missing]"
empty=[] missing=[]Indistinguishable from the outside — which is usually fine, and occasionally exactly the bug. Chapter 8.2 gives you a way to tell them apart.
unset removes a variable entirely:
$ v=1
$ unset v
$ echo "after unset: [$v]"
after unset: []Catching the typo
The default behaviour — an unset variable silently expands to nothing — is the single most common cause of a script that destroys the wrong thing. rm -rf "$TMPDIR/"* with TMPDIR misspelled becomes rm -rf /*.
Bash can be told to refuse instead:
$ set -u
$ echo "[$missing]"
bash: missing: unbound variableset -u makes expanding an unset variable an error. For anything that will be run unattended it is close to mandatory; it turns a silent catastrophe into a loud, early, specific failure.
(It is usually written as part of set -euo pipefail — Chapter 10.6 explains the set command itself, and its other two parts belong to Module 11 and Module 12. Chapter 20.2 covers what each one genuinely does and — importantly — what it does not.)
Fixing a value in place
$ readonly R=fixed
$ R=changed
bash: R: readonly variablereadonly prevents further assignment. It is worth using for the two or three values at the top of a script that define where everything else lives, precisely because those are the ones whose corruption does the most damage.
7. Variables Belong to One Shell
Everything in Chapter 7.4 about inheritance applies directly, and is worth restating in its new vocabulary:
$ LOCAL=hello
$ bash -c 'echo "child sees: [$LOCAL]"'
child sees: []
$ export SHARED=hello
$ bash -c 'echo "child sees: [$SHARED]"'
child sees: [hello]A plain assignment makes a shell variable, which lives in one shell process. export additionally places it in the environment, which is what children receive a copy of. Changes a child makes never return.
That is also why a variable set in a script you run is gone afterwards, and a variable set in a script you source is not — the same one-way rule, seen from the variable's side.
8. Using Variables for Real
The point of all this is to stop repeating yourself. Here is a sequence most engineers type in some form every week:
project=/home/engineer/project
run=run_42
logdir=$project/sim/logs
mkdir -p "$logdir"
cp "$project/rtl/alu.sv" "$project/sim/"
echo "starting $run"Four things that improved:
- The project path is written once. Moving the work means editing one line.
- The run name is written once, so there is no chance of
run_42in one place andrun_43in another. logdiris built fromproject, so it cannot drift out of step with it.- Every expansion that reaches a command is quoted. The paths here contain no spaces — but the next project's might, and the script should not care.
Note logdir=$project/sim/logs: a variable's value can be built from other variables, and the right-hand side of an assignment is one of the few places where omitting the quotes is safe, because no word splitting happens there. Quoting it anyway costs nothing and keeps the habit intact.
Practice
1 — Meet all three spacing errors. Type each of these and read the message before fixing it:
name = run_42
name =run_42
name= run_42
name=run_42Then explain to yourself why the third one names a different word.
2 — Prove the value is text.
n=5
echo "$n+1"
echo "$(( n + 1 ))"3 — Make the splitting failure happen, then fix it.
f="my report.txt"
touch "$f"
ls -l $f # two errors
ls -l "$f" # one file
rm "$f"4 — Build a path from parts.
project=/home/engineer/project
sub=sim/logs
echo "$project/$sub"5 — Tell unset from empty, before reading 8.2. Set one variable to the empty string and leave another unset. Confirm echo cannot distinguish them. Then turn on set -u in a subshell so you do not have to live with it, and watch only one of them fail:
e=""
( set -u; echo "[$e]"; echo "[$nope]" )6 — Protect a value.
readonly PROJECT=/home/engineer/project
PROJECT=/tmp
echo "$PROJECT"7 — Confirm the export boundary. Set one variable plainly and one with export, then run bash -c 'echo ...' for each.
8 — Check the convention. Run env | head -20. Count how many are uppercase.
Check Your Understanding
1. count = 5 reports count: command not found. Why does the message name count rather than complaining about the equals sign?
2. n=10 and then echo $n/2 prints 10/2. Is this a bug?
3. You have f="Q3 report.txt" and run wc -l $f. What does wc actually receive, and what will it report?
4. Why can you write logdir=$project/sim without quotes, but not mkdir $logdir?
5. A script does rm -rf "$BUILDDIR"/* and BUILDDIR is misspelled. What happens, and what one line would have prevented it?
6. MARKER=x in a script you run with ./setup.sh does not exist afterwards. export MARKER=x does not either. Why not, and what does work?
9. Remember
- Assign without a dollar sign, read with one:
name=run_42, then"$name". - No spaces around the
=. With a space the line stops being an assignment and becomes a command. name= valueis valid syntax doing something else — runningvaluewithnameset for that command only. That is why its error names the wrong word.- Every value is text.
n=5stores a character, not a number; arithmetic must be requested with$(( )). - Expansion happens before the command runs. The command never sees
$name, only what replaced it. - Quote every expansion:
"$name". The unquoted form works until a value contains a space, and then fails by inventing arguments. - Names are letters, digits and underscores, not starting with a digit, and are case-sensitive.
UPPERCASEfor environment variables, lowercase for your own — a convention worth following so you never collide withPATH.- Unset and empty both expand to nothing. Chapter 8.2 tells them apart.
set -uturns an unset variable into an error, which is the difference between a script that stops and a script that deletes the wrong directory.readonlyfixes a value;unsetremoves one.- A plain assignment is local to one shell;
exportputs it in the environment that children inherit — and nothing a child does comes back.
That is one variable holding one piece of text. The next chapter is about the forms that go around the name — how to say "use this, or this default if it is not set", how to get a value's length, and how to attach an expansion directly to other text without the shell reading it as one long name.
Where this fits
Part of the Linux & Bash curriculum.
