Variables and Quoting🔗
Part of Essentials — Bash Scripting
Second in the Essentials Bash series. Assumes you've read Your First Bash Script and are comfortable with basic script structure. Next up: Arguments and Exit Codes.
rm -rf $target/ works fine in testing. Set $target to an empty string in production — even accidentally — and it becomes rm -rf /. That's an unquoted variable bug. Quoting is how you prevent a script from doing something catastrophic with unexpected input. This article covers the rules once, so they stick.
Where You've Seen This🔗
If you've set environment variables in Windows (System Properties → Environment Variables) or seen %APP_PATH% in a batch file, the concept is identical — a named value the shell expands on demand. Bash uses $VAR instead of %VAR%, and name="value" with no spaces around =; that space is the first thing most people get wrong.
Declaring Variables🔗
Variables let you name a value once and use it everywhere in a script. Change it in one place and every reference picks up the change — no hunting for hardcoded paths or repeated hostnames.
| Variable Assignment | |
|---|---|
- Strings go in double quotes.
- Integers don't need quotes.
- Paths in double quotes — safe against word splitting if the value ever changes.
The one rule that trips everyone up: no spaces around =.
| What NOT to Do | |
|---|---|
- Bash treats
app_nameas a command,=as its first argument, and"myapp"as its second. Result:app_name: command not found.
Expanding Variables🔗
To use a variable's value, prefix it with $. Braces are optional but recommended in scripts — they make boundaries explicit and prevent ambiguity:
| Variable Expansion | |
|---|---|
- Simple expansion — works, but
$app_namesuffixwould try to expandapp_namesuffix, notapp_name. - Braces make the boundary explicit —
${app_name}suffixexpands correctly. - Double quotes: the variable expands normally.
- Single quotes: everything is literal — prints
${app_name}, not the value.
Quoting Rules🔗
This table is worth memorising:
| Context | Variable expands? | Word splitting? |
|---|---|---|
Unquoted ($VAR) |
✅ Yes | ✅ Yes — splits on whitespace |
Double quotes ("$VAR") |
✅ Yes | ❌ No |
Single quotes ('$VAR') |
❌ No | ❌ No |
The default: use double quotes around variables in scripts. Single quotes are for literal strings where you explicitly don't want expansion.
Why Unquoted Variables Break🔗
Word splitting means Bash splits the variable's value on whitespace and treats each piece as a separate argument. An empty variable expands to nothing — silently dropping the argument entirely:
| Word Splitting | |
|---|---|
- Passes two arguments:
connectionas the log message,refusedas a syslog tag. Wrong. - Passes one argument:
connection refusedas the message. Correct.
Where Variables Come From🔗
In any running script, variables arrive from three sources. Knowing the source is the fastest way to debug a variable that isn't what you expect.
1. Your script — Variables you declare yourself. Use these for any value you compute, configure, or reference more than once:
- Variables can reference other variables.
${app_name}expands at the time this line runs.
2. The environment — Variables inherited from whoever ran the script — the shell, a service manager, or a calling process. $HOME, $USER, and $PATH are always present. Custom variables (API keys, config paths) arrive this way from calling scripts or automation systems:
| Environment Variables | |
|---|---|
- Your home directory — set by the shell at login.
- The current user's login name.
- Colon-separated list of directories Bash searches for commands — see Filesystem Hierarchy.
exportmakes a variable visible to any child process this script starts. Without it, child processes don't see the variable.
3. Bash itself — Set automatically by Bash, not by you. Read them; don't assign to them:
- The script's name as called — useful in usage messages and error output.
- The current script's process ID.
- Exit code of the last command: 0 = success, non-zero = failure.
These are covered in full in Arguments and Exit Codes.
You can also mark any of your own variables readonly to prevent reassignment — useful for values that must not change after initialisation:
Command Substitution🔗
Some values only exist at runtime — today's date, current disk usage, a server's hostname. Command substitution captures a command's output as a string you can assign to a variable or embed inline:
| Command Substitution | |
|---|---|
- Captures today's date in
YYYY-MM-DDformat. - The entire pipeline runs in a subshell; only the final output is captured.
- Counts non-comment lines in
/etc/hosts— a pipeline of arbitrary complexity works here.
Always quote the result — command output can contain spaces or be empty:
| Quote Command Substitution Output | |
|---|---|
- If
outputcontains spaces it splits into multiple arguments; if empty the argument disappears entirely. - Always quoted — one argument regardless of what the command returns.
The backtick syntax (`command`) is equivalent but harder to read and can't be nested cleanly. Use $(...).
Default Values🔗
Scripts run in different environments. On your workstation LOG_DIR is set. On a colleague's machine or in a CI run it isn't. Default value operators handle both cases without if/else logic:
| Default Value Operators | |
|---|---|
- Use the default if
LOG_DIRis unset or empty. - Use the default only if
LOG_DIRis unset — an empty string is kept as-is. - Set
DEPLOY_ENVtoproductionif unset or empty, and keep it set. The:command discards the value; the side effect is the assignment. - Exit with an error if
API_KEYis unset or empty. Use this at the top of scripts that require specific variables.
| Script with Configurable Defaults | |
|---|---|
Run as-is for defaults, or override at call time: LOG_DIR=/tmp/test ./deploy.sh
| Validate Required Variables | |
|---|---|
Put these at the top of any script that depends on external variables — it fails immediately with a clear message rather than silently using empty values deep in the script.
Practice Exercises🔗
Exercise 1: Diagnose the Quoting Bug
This script has a dangerous quoting bug. Identify it and fix it:
Solution
$target_dir is unquoted. If $1 is empty — no argument was passed — $target_dir expands to nothing and the command becomes rm -rf /cache. Quote it, and validate the argument is set before using it:
| Fixed Script | |
|---|---|
The :? operator exits with an error if the variable is unset or empty.
Exercise 2: Build a Deployment Info Script
Write a script that:
- Captures the current git branch with
git rev-parse --abbrev-ref HEAD - Captures the current commit hash with
git rev-parse --short HEAD - Captures the current timestamp with
date "+%Y-%m-%d %H:%M:%S" -
Prints a formatted header:
Quick Recap🔗
name="value"— no spaces around=; a space makesnamea command- Always quote variable expansions:
"${VAR}"— prevents word splitting and empty-argument bugs - Double quotes expand variables; single quotes are literal
- Variables come from your script, the environment, or Bash itself — knowing the source speeds up debugging
$(command)captures command output — quote the result like any other variable${VAR:-default}for optional config;${VAR:?message}for required variablesexport VARmakes a variable visible to child processes;readonly VARprevents reassignment
What's Next?🔗
Head to Arguments and Exit Codes — $1, $@, $#, exit codes, and how to make scripts composable with everything that calls them.
Further Reading🔗
Command References🔗
man bash— search the "Parameter Expansion" section for the full list of${VAR:-}operatorsman env— howenvworks in shebangs and for setting per-command environment variables
Deep Dives🔗
- Bash Pitfalls — BashFAQ's list of common mistakes; many are quoting-related
- Bash FAQ: Quoting — the Wooledge wiki's definitive guide to quoting
Official Documentation🔗
- GNU Bash Manual: Shell Parameters — all special variables
- GNU Bash Manual: Command Substitution
Exploring Python🔗
- Environment Variables and Secrets — Managing
os.environ,.envfiles, and secrets safely in Python scripts: what the Python side looks like once your config needs grow past$VAR