Shell Variables
A script without variables is just a fixed sequence of commands — useful once, then thrown away. Variables are what let a script adapt: store a path, remember a result, build up a value across several steps. Bash’s variable handling looks deceptively simple at first glance, but a few of its rules — what counts as a valid name, how quoting changes what gets stored, and the split between “shell variable” and “environment variable” — trip up scripts constantly if you don’t have them straight from the start. This chapter covers all of that, plus printf, the more reliable alternative to echo for producing output.
Assigning Variables
Bash variable assignment has one strict, easy-to-forget rule: no spaces around the =.
name="Alice"name = "Alice" # this does NOT workThat second form doesn’t assign anything — Bash instead tries to run a command called name with arguments = and Alice", which fails. Spacing around = is the single most common syntax mistake newcomers to Bash make, precisely because it looks fine in almost every other language.
Once assigned, reference a variable’s value with a $ prefix:
name="Alice"
echo "$name"AliceYou can also write ${name} — the braces are optional here, but become necessary when a variable name is immediately followed by other characters that would otherwise be read as part of the name:
file="report"
echo "$file.txt" # works fine — . isn't a valid name character
echo "${file}_final" # braces required — file_final would be parsed as one variable nameWithout the braces in that second example, Bash would look for a variable literally named file_final, find nothing, and print an empty string instead of report_final.
Multiple assignments can appear on the same line, separated by spaces — each one is still a separate assignment with no space around its own =:
first="Alice" last="Smith"A variable can also be declared with no value at all, which gives it an empty string:
placeholder=
echo "[$placeholder]"[]Naming Conventions
Bash variable names must start with a letter or underscore, and contain only letters, digits, and underscores after that. Names are case-sensitive — path and PATH are different variables entirely (and, as you’ll see shortly, that particular pair matters a lot).
Bash itself doesn’t enforce any casing convention, but the ecosystem has a strong one worth following:
UPPER_CASEis conventionally used for environment variables and constants — values meant to be exported, or that shouldn’t change during the script’s run.lower_caseis conventionally used for ordinary variables local to your script’s logic.
Following this convention matters for a practical reason beyond readability: a long list of UPPER_CASE names are already claimed by the shell and the system (PATH, HOME, USER, SHELL, IFS, and many more). Reassigning one of these by accident — because you picked a “sensible” all-caps name without checking — can break command lookup, break your prompt, or worse, depending on which one you clobber. Sticking to lower_case for your own script-local variables sidesteps this entirely.
Quoting Basics
How you quote a value changes what actually gets stored or expanded. This is only a first look — quoting has enough sharp edges to earn its own dedicated chapter later in this course — but the core distinction is essential from day one:
Double quotes ("...") preserve the literal text, but still allow variable expansion and command substitution inside them:
greeting="Hello"
message="$greeting, world"
echo "$message"Hello, worldSingle quotes ('...') preserve everything completely literally — no expansion of any kind happens inside them:
greeting="Hello"
message='$greeting, world'
echo "$message"$greeting, worldNotice $greeting was never expanded in the single-quoted version — it printed exactly as typed. This is the rule to internalize now: double quotes expand, single quotes don’t. You’ll use both constantly, and picking the wrong one is a common source of Bash bugs — for example, forgetting single quotes prevent expansion when you actually wanted a literal $ in a string, or forgetting double quotes are needed when you do want a variable’s value substituted in.
Note
Unquoted assignment (name=$other_var) is safe from most of the pitfalls that come up when referencing variables elsewhere in a command — assignment is a single-word context, so word splitting doesn’t apply the same way there. The dangerous cases show up when you reference a variable’s value later, inside a command, without quotes — covered under Shell-Safety Considerations below.
Shell Variables vs. Environment Variables
Every variable you assign with name=value starts life as a shell variable — it exists only inside your current shell (or script) and is completely invisible to any other process, including child processes that shell spawns.
An environment variable is a shell variable that’s been marked for export — meaning it gets copied into the environment of every child process that shell starts from that point forward. This is the same environment mechanism you’re already relying on every time a command reads $PATH to find programs, or reads $HOME to locate your home directory.
The distinction matters because it explains behavior that otherwise looks mysterious: a variable set in your script is completely invisible to any other script or program that script runs, unless it was explicitly exported first.
Exporting Variables
Use export to promote a shell variable into the environment:
export api_url="https://example.com/api"or, if the variable already exists:
api_url="https://example.com/api"
export api_urlOnce exported, api_url is copied into the environment of any child process this shell starts — including other scripts run with ./script.sh or bash script.sh (both of which, as covered in the previous chapter, run in a new child process). A shell variable that was never exported won’t be visible there, no matter how the child script tries to read it.
You can see the full current environment with:
envor check a single variable’s environment status with:
printenv api_urlIf printenv prints nothing (and exits with a nonzero status), that variable either doesn’t exist or was never exported.
Making Variables Read-Only
readonly marks a variable so it can no longer be reassigned or unset for the rest of the shell’s (or script’s) lifetime:
readonly max_retries=5max_retries=10bash: max_retries: readonly variableThis is useful for values that represent genuine constants within a script — configuration values, fixed thresholds, anything you want to guarantee doesn’t accidentally change partway through execution. Attempting to reassign a readonly variable produces an error and, inside a script, will halt that script’s naive line-by-line progress on that statement (though not necessarily the whole script, depending on how it’s structured — a topic covered more fully once you reach error-handling later in the course).
List all currently read-only variables with:
readonly -pUsing printf For Output
echo is fine for quick, simple output, but it has inconsistent behavior across systems for things like escape sequences and flags, and it silently treats leading dashes in its arguments as options rather than text. printf avoids all of that by requiring an explicit format string, the same way C’s printf does:
name="Alice"
printf "Hello, %s\n" "$name"Hello, AliceCommon format specifiers:
| Specifier | Meaning |
|---|---|
%s | String |
%d | Integer |
%f | Floating-point number |
%% | Literal percent sign |
Unlike echo, printf never adds a trailing newline automatically — you have to include \n yourself, as shown above. This is a deliberate design choice that makes printf predictable: what you write in the format string is exactly what you get, with no platform-dependent surprises.
printf also handles values safely that would confuse echo:
value="-n"
echo "$value"
printf "%s\n" "$value"-nDepending on your system’s echo implementation, echo "$value" above can silently interpret -n as the “no trailing newline” flag instead of printing it as text. printf "%s\n" "$value" never has this ambiguity, because -n is being passed as data to fill %s, not interpreted as an option at all.
Best Practices
- Use
lower_casefor your own script-local variables and reserveUPPER_CASEfor values you intend to export or that represent true constants — this avoids accidental collisions with well-known environment variables likePATHorHOME. - Prefer
printfoverechowhenever a value’s exact content matters — especially for anything coming from user input, a file, or command output, where you can’t guarantee it won’t start with a dash or contain unexpected characters. - Use
readonlyfor genuine constants in a script — it documents intent and catches accidental reassignment early, rather than letting a typo silently overwrite a value you meant to keep fixed. - Export deliberately, not by habit. Only export variables that child processes actually need to see. Exporting everything “just in case” pollutes the environment every subsequent command inherits, and makes it harder to reason about where a value actually came from.
Shell-Safety Considerations
The quoting basics above aren’t just a style preference — skipping quotes when referencing a variable can change what a command actually does. Here’s a naive script that looks correct but breaks the moment a value contains a space:
#!/usr/bin/env bash
filename="my report.txt"
touch $filenameRun this, then check what actually got created:
lsmy report.txtTwo files, not one — my and report.txt. Because $filename was unquoted, Bash performed word splitting on its value before passing it to touch: the space inside "my report.txt" was treated as a separator between two separate arguments, exactly as if you’d typed touch my report.txt directly.
The fix is to quote the reference:
#!/usr/bin/env bash
filename="my report.txt"
touch "$filename"lsmy report.txtOne file, with the space intact, exactly as intended. The rule to carry forward from this chapter: quote your variable references by default, especially anywhere a value might contain spaces, and treat leaving a reference unquoted as something you do deliberately, with a specific reason — not out of habit. You’ll see the full depth of this behavior, including how it interacts with globbing and empty values, in the dedicated quoting chapter later in the course.
Hands-On: Assignment, Quoting, Exporting, And Read-Only Variables
This walkthrough builds one small script that exercises every mechanism covered above, including the word-splitting bug and its fix.
1. Set up a working directory.
mkdir -p ~/shell-course/ch2
cd ~/shell-course/ch22. Create a script demonstrating basic assignment and printf. Save this as basics.sh:
#!/usr/bin/env bash
first_name="Alice"
last_name="Nguyen"
full_name="$first_name $last_name"
printf "Full name: %s\n" "$full_name"
printf "First initial: %s\n" "${first_name:0:1}"Run it:
chmod +x basics.sh
./basics.shFull name: Alice Nguyen
First initial: A3. Reproduce the word-splitting bug, then fix it. Save this as unsafe-touch.sh:
#!/usr/bin/env bash
filename="my report.txt"
touch $filename
echo "Files created:"
lschmod +x unsafe-touch.sh
mkdir -p /tmp/shell-course-demo
cd /tmp/shell-course-demo
bash ~/shell-course/ch2/unsafe-touch.shFiles created:
my report.txtConfirm two separate files exist, then clean the demo folder and fix the script:
rm -f *Edit unsafe-touch.sh (in ~/shell-course/ch2/) so the touch line reads touch "$filename", then run it again from the same demo folder:
bash ~/shell-course/ch2/unsafe-touch.shFiles created:
'my report.txt'One file this time, space and all.
4. Demonstrate exported vs. non-exported variables. Back in your course directory, create a child script and a parent script:
cd ~/shell-course/ch2
cat > child.sh << 'EOF'
#!/usr/bin/env bash
echo "In child script:"
echo " visible_var = $visible_var"
echo " hidden_var = $hidden_var"
EOF
chmod +x child.sh
cat > parent.sh << 'EOF'
#!/usr/bin/env bash
export visible_var="I was exported"
hidden_var="I was not exported"
./child.sh
EOF
chmod +x parent.sh
./parent.shIn child script:
visible_var = I was exported
hidden_var = hidden_var shows up empty inside child.sh — it never left parent.sh’s shell, because it was never exported.
5. Try reassigning a read-only variable.
readonly build_id="20260822-01"
build_id="something-else"bash: build_id: readonly variable6. Clean up.
cd ~
rm -rf ~/shell-course/ch2
rm -rf /tmp/shell-course-demoYou’ve now assigned and referenced variables correctly, seen exactly how unquoted references can silently break a command, exported a value across a process boundary, and locked a variable against reassignment. The next chapter moves to command execution itself — how Bash distinguishes builtins from external commands, how PATH lookup actually works, and what exec does differently from a normal command.