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
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
What's Next?
Head to Arguments and Exit Codes — $1, $@, $#, exit codes, and how to make scripts composable with everything that calls them.