Module 2 — Variables, Quoting, and Word Splitting

Updated 3 September 2026

Module 2 — Variables, Quoting, and Word Splitting. Unquoted variables are the single biggest source of bash bugs in production — this module makes quoting a reflex, not a rule you look up.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Module 1: running commands, creating and executing scripts with a shebang and chmod +x, and the fact that bash rewrites your command line before running it (Module 1's restaurant analogy promised details — this module delivers them). Tools: the same terminal and nano; nothing new to install. Keep your ~/bash-course practice directory — we build on it.

Part A — Variables: making a script remember

A1. Creating and reading a variable

A variable is a named box that holds one piece of text. You create it with a single =:

bash
name="Ada"        # create a variable called name, containing the text Ada

And you read it by writing $ in front of the name. When bash sees $name on a command line, it replaces it with the box's contents before running the command — this replacing is called expansion, and it is the first piece of Module 1's "the shell rewrites your order" machinery:

bash
echo "Hello, $name"    # bash rewrites this to: echo "Hello, Ada" — then runs it

Two rules that surprise everyone. First: no spaces around the =. Second: the $ is only for reading. Writing $name="Ada" or name = "Ada" are both errors — and the error messages are confusing enough that we will trigger one on purpose in a moment.

Names may contain letters, digits and underscores, and cannot start with a digit. Convention: lowercase names for your script's own variables (retries, logfile), UPPERCASE for system-provided ones (PATH, HOME) — following it prevents you from accidentally overwriting something the system depends on.

Counter-intuitive: why does name = "Ada" fail with name: command not found rather than a helpful "did you mean name=?" — because to bash, spacing is meaning. The very first word of a command line is the command; everything after is its inputs. Write name = "Ada" and the first word is name — so bash goes looking for a program called name (Module 1's PATH search!), fails, and reports it. Bash never guesses your intent from context the way a human reader would. The whole language works like this: where you put spaces and quotes is the grammar.
Real-world analogy — labeled jars. A variable is a labeled jar on a kitchen shelf. name="Ada" writes a label and puts something in the jar. $name in a recipe means "whatever is in the jar labeled name, right now" — the cook (bash) opens the jar and pours the contents into the dish before cooking starts. The recipe never contains the jar; it contains what was poured out.

Where the analogy stops working. Jars persist; variables do not. A variable lives only inside the one running shell that created it — close the terminal (or let a script finish — its shell dies, Module 1 C1) and the jar never existed. There is no saved file anywhere. That is also why a variable set in one terminal is invisible in another: two shells, two separate shelves.

🧪 Exercise 2.1 — a box, and two classic mistakes

Two of these lines fail on purpose.

bash
name="Ada"
echo "Hello, $name"
name = "Ada"      # ← fails: spaces around =
echo "Hello, $Name"   # ← "fails" silently: wrong capitalization
Expected result — click to reveal (contains deliberate failures)
javascript
Hello, Ada
bash: name: command not found
Hello,

What to read out of it: line 1 — the expansion worked; echo received Hello, Ada. Line 2 — the spaced = made bash treat name as a command to search for: command not found (recognize that error from Module 1? Same mechanism, PATH search — nothing to do with variables, which is exactly why it confuses people). Line 3 is the more dangerous failure: $Name (capital N) is a jar that was never filled, and bash expands an unset variable to nothing, silently — no error, no warning, just Hello, and a trailing space — the space you typed survived; only the variable expanded to nothing. Variable names are case-sensitive. Remember this silence: Module 14 teaches set -u, the production switch that turns it into a loud error.

A2. ${name} — braces for when text touches the variable

$file and ${file} mean the same thing. The braces exist because bash must decide where a variable's name ends, and its rule is "swallow every following letter, digit, and underscore." Write $file_backup hoping for "the variable file, then the text _backup" and bash instead reads a variable named file_backup — unset, so it silently expands to nothing (A1's silence, striking again). The braces draw the boundary: ${file}_backup.

Real-world analogy — the address on the parcel. Writing a variable into surrounding text is addressing a parcel: "Flat 4B, 12 Ada Street." Without punctuation, is the flat "4B" or "4B12"? The reader's greedy eye keeps attaching characters. Braces are the comma — an explicit "the name stops here."

Where the analogy stops working. A human puzzled by an ambiguous address asks the sender. Bash resolves the ambiguity by silent rule (longest legal name wins), builds a wrong address, and delivers the parcel to nobody — with no returned mail. You only find out when something downstream is empty.

🧪 Exercise 2.2 — the swallowed name

The middle line fails silently on purpose.

bash
file="report"
echo "$file_backup"      # ← wrong: bash reads one variable named file_backup
echo "${file}_backup"    # right: braces mark where the name ends
Expected result — click to reveal (contains a silent failure)
javascript

report_backup

What to read out of it: the first echo prints an empty line — not the word "report" with a suffix, not an error. file_backup is a perfectly legal (and empty) variable name, so bash had nothing to complain about. The second line is what was intended. The habit to build: the moment a variable's value has text glued directly after it, reach for braces. (Before it is fine: backup_$file works, because _ before $ cannot be swallowed backwards.)

A3. Command substitution: a variable filled by a command

So far you have filled jars by typing the contents. The second way is to have a command's output poured in — that is $(command), called command substitution: bash runs the command inside the parentheses first, captures everything it prints, and substitutes that text in place. It is how scripts capture the date, the hostname, a count, a version — anything a command can print:

bash
now=$(date)         # run date, store its printed output
echo "Backup started at: $now"

You will also meet an older spelling in legacy scripts and interview questions: backticks — now=date . It does the same job, but nesting one inside another is painful and the backtick character is easy to misread as a single quote. Modern scripts use $( ); you need only recognize the backticks.

Real-world analogy — the runner. Command substitution is sending a runner out mid-sentence: "Book the meeting room for — runner, go check how many people are coming — that many people." The runner (the inner command) completes their errand first, comes back with a slip of paper, and the slip's contents are spliced into your sentence before it is spoken.

Where the analogy stops working. A runner reports back speech; the inner command's entire printed output gets spliced in — including any error text it printed, and with trailing newlines trimmed. If the runner fails and prints nothing, your sentence just has a hole where the number should have been, and bash reads it out anyway (the silent-emptiness theme of this module, a third time).

🧪 Exercise 2.3 — fill variables from commands
bash
now=$(date)
echo "Backup started at: $now"
host=$(hostname)     # hostname prints this machine's name
echo "Running on: $host"
Expected result — click to reveal
javascript
Backup started at: Thu Sep  3 12:25:58 +08 2026
Running on: zaeems-machine

What to read out of it: your date and machine name will differ, but the shape is the point — $now expanded to the whole line date printed, spaces and all, and it slotted into the sentence. Notice what you did not see: running now=$(date) printed nothing. The output went into the jar instead of onto the screen. A command substitution captures output; only the later echo put it on screen.

Interview questions — Part A

🎯 "What do you mean by Shell variable?" — asked verbatim at InterviewBit; LinuxTeck asks "How do you create a variable in a script?"

The direct answer: a named piece of text stored by the shell, created with name=value (no spaces around =), read with $name. Variables exist only in the shell that created them and vanish when it exits.

Going deeper: mention both fill methods — literal assignment and command substitution name=$(command) — and both read forms, $name and ${name} with braces for boundary control. Name the two-class convention: lowercase for script-local variables, UPPERCASE for environment/system ones.

The details that separate candidates: stating that expansion happens before the command runs — the command only ever sees the substituted text, never the variable — and that an unset variable expands to nothing silently by default. Both facts predict whole families of bugs, and interviewers notice candidates who reason from them.

🎯 "What are different types of variables mostly used in shell scripting?" — asked verbatim at InterviewBit and Edureka

The expected answer: two types. System-defined (environment) variables, created by the OS or login machinery, conventionally UPPERCASE — PATH, HOME, USER, SHELL; and user-defined variables, created by you in a script or session.

Going deeper: the deeper split is not who created the variable but whether it is exported — exported variables are copied into every program the shell starts; plain variables stay private to the shell. Part C of this module demonstrates that difference with a two-line experiment.

The details that separate candidates: printenv shows only environment variables while set shows all of them — naming the two commands, and why their outputs differ, shows hands-on familiarity rather than a memorized taxonomy.

🎯 "What is command substitution in shell scripting?" — asked verbatim at PlacementPreparation and Zero To Mastery

The direct answer: $(command) runs the command and replaces itself with the command's output — letting you store output in variables (now=$(date)) or embed it in other commands. The legacy backtick form does the same.

Going deeper: know the mechanics — trailing newlines are stripped from the captured output; the substitution runs in a subshell (so it cannot set variables for your script — a classic gotcha, fully explained in Module 12); and $( ) nests cleanly ($(dirname $(which bash))) where backticks need escaping.

The details that separate candidates: the quoting connection — echo "$(date)" is safe, unquoted $(...) output undergoes the word splitting taught in Part B of this module; and knowing why modern style bans backticks (nesting, readability) rather than just following it.

Part B — Word splitting and quoting: where bash bugs are born

B1. What really happens to an unquoted variable

Here is the mechanism this whole module exists to teach. When bash rewrites your command line, expansion (A1) is not the last step. After expanding an unquoted $var, bash takes the result and splits it into separate words at every space, tab, and newline. Each word then becomes a separate argument to the command.

Feel the consequence. Suppose file="monthly report.txt" — one filename, containing a space. Then:

ls -l $file → expand → ls -l monthly report.txt → split → ls receives two arguments: monthly, and report.txt. Neither exists. Two errors, about two files you never mentioned.

ls -l "$file" → the double quotes tell bash: expand, but do not split the result — pass it as one word. ls receives one argument: monthly report.txt. It works.

The maddening part: both spellings behave identically as long as values contain no spaces — so the unquoted habit passes every test on clean data, then detonates months later on the first filename with a space in it. That is why this is the number-one bash bug in production, and why the fix is a reflex, not a diagnosis.

Counter-intuitive: the splitting happens after expansion, so the command being run has no way of knowing the two words were ever one value. ls did not "mishandle the space" — ls genuinely received two separate arguments, and bash genuinely believed it was doing its job. Quotes are not decoration around strings, and not "for when there are spaces": they are an instruction to bash about which of its own rewriting steps to skip. That reframe — quotes control the rewriting machinery, they do not mark text — is the single most valuable idea in this module.
Real-world analogy — the order shouted to the kitchen. The waiter (bash, Module 1) relays your order to the kitchen by shouting each item separately. Unquoted, the note "cheese sandwich" is shouted as two orders: "cheese!" — "sandwich!" — and the kitchen makes two wrong dishes. Quoting is handing the note over in an envelope: whatever is inside arrives as exactly one order, spaces and all.

Where the analogy stops working. A kitchen receiving "cheese!" and "sandwich!" would smell the mistake and ask. Commands cannot: by the time ls runs, the argument boundaries are simply facts, indistinguishable from two files you really did name. There is no "original sentence" left anywhere for anyone to check against.

🧪 Exercise 2.4 — one file becomes two arguments

The third command fails on purpose.

bash
mkdir -p ~/bash-course/quoting-lab && cd ~/bash-course/quoting-lab   # && means: run the second command only if the first succeeded (previewed here; taught fully in Module 3)
touch "monthly report.txt"    # touch creates an empty file — quoted, so it is ONE file with a space in its name
file="monthly report.txt"
ls -l $file        # ← unquoted: fails twice
ls -l "$file"      # quoted: works
Expected result — click to reveal (contains a deliberate failure)
javascript
ls: cannot access 'monthly': No such file or directory
ls: cannot access 'report.txt': No such file or directory
-rw-r--r-- 1 zaeem zaeem 0 Sep  3 12:26 'monthly report.txt'

What to read out of it: the unquoted run produced two error lines — the proof of the mechanism. Read the names in the errors: monthly and report.txt, the two fragments after splitting; ls itself tells you exactly where bash cut. The quoted run lists one file (size 0 — touch creates empty files; owner and date are yours). Note ls prints the name in quotes itself — modern ls does that for names with spaces, as a warning to script authors. If it fails with "No such file or directory" on both halves of a name you can see with plain ls, that is word splitting — every time.

Now imagine this at 500 hosts. On your laptop, an unquoted $file costs an error message. In a fleet-wide cleanup script — rm $old_backup — a value that unexpectedly splits can name files you never intended to touch, on every host at once. Real-world postmortems of "script deleted the wrong thing" incidents very often reduce to an unquoted expansion meeting an unexpected space. Production style guides (and the ShellCheck linter you will meet in Module 14) therefore treat every unquoted $var as a defect, even when today's values are space-free — because values change and scripts outlive their assumptions.

B2. Double quotes vs single quotes

Both kinds of quotes suppress word splitting. The difference is what else they suppress:

You writeExpansion ($)SplittingUse it for
"double quotes"still happenssuppressedthe default — text that mixes words and variables
'single quotes'suppressed — $ is a plain charactersuppressedtext that must stay exactly as typed

A rhyme that sticks: double quotes are see-through, single quotes are solid. Bash looks through double quotes and still does $ work inside; single quotes are a solid wall — every character inside is literal, no exceptions (even a backslash). And for one lone special character in otherwise plain text, there is a third tool: a backslash escapes exactly the next character — \$ is a literal dollar sign.

Real-world analogy — the fill-in-the-blanks letter. A double-quoted string is a mail-merge template: "Dear name, your bill is attached" — the office fills the blanks before sending. A single-quoted string is a photocopy: what you wrote is exactly what arrives, blanks and all. You photocopy when the blanks are *meant for someone else to fill in* — which is precisely why single quotes wrap text bound for other programs that have their own `` syntax (awk and regular expressions, coming in Module 11).

Where the analogy stops working. You cannot nest a photocopy inside itself: there is no way to escape a single quote between single quotes — 'It's' breaks, because the middle ' ends the string. The practical fix is to switch tools: wrap the whole thing in double quotes instead ("It's"), since ' has no special meaning there.

🧪 Exercise 2.5 — see-through vs solid
bash
name="Ada"
echo "Double: $name"     # see-through: $name is expanded
echo 'Single: $name'     # solid: $name is four characters and a dollar sign
echo "Price: \$5"        # backslash: just this one $ is literal
Expected result — click to reveal
javascript
Double: Ada
Single: $name
Price: $5

What to read out of it: line 2 is the one to stare at — the characters $name survived to the screen, untouched, because single quotes stopped expansion itself. Line 3 shows the surgical option: inside double quotes, \$ printed a real dollar sign while the rest of the string stayed dynamic. Choose by intent: mixing in values → double; exact text → single; one literal special character → backslash.

B3. The third splitter: * and filename expansion

One more rewriting step hits unquoted text: filename expansion (also called globbing). An unquoted word containing * is replaced by the list of filenames it matches in the current directory — * matches anything, *.txt matches names ending .txt. This is wonderful when you mean it (ls *.txt) and treacherous when a * arrives inside a variable's value, because after unquoted expansion, bash runs the glob step on the result — your data just became a pattern. Double quotes suppress globbing too, which is why the same "$var" reflex covers this hazard without any extra rule.

🧪 Exercise 2.6 — when data becomes a pattern

The second command misbehaves on purpose (no error — worse, a wrong answer).

bash
cd ~/bash-course/quoting-lab
touch notes1.txt notes2.txt
msg="*"            # imagine this arrived from a log line or user input
echo $msg          # ← unquoted: bash turns your data into a file listing
echo "$msg"        # quoted: the data stays data
Expected result — click to reveal (contains a deliberate misbehavior)
javascript
monthly report.txt notes1.txt notes2.txt
*

What to read out of it: the unquoted echo never printed your variable at all — bash expanded $msg to *, then glob-expanded * to every filename in the directory, and echo innocently printed that list. No error anywhere, just wrong output. The quoted echo prints the actual value: one asterisk. Now replay B1's fleet warning with this mechanism: rm $pattern where the value unexpectedly contains *… — this is why "quote everything" is not pedantry.

B4. The reflex

The rule that production bash lives by, stated once, plainly: every $var and every $(command) gets double quotes, everywhere, always — "$var", "$(command)" — unless you can say out loud why you need the splitting or globbing. Legitimate exceptions exist (you will meet real ones in Modules 6 and 10), but they are rare enough that each one deserves a comment in the script explaining itself. The reflex costs two keystrokes; the bug it prevents costs an incident review.

Interview questions — Part B

🎯 "Differentiate between ' and " quotes." — asked verbatim at Edureka; LinuxTeck asks "What is the difference between single and double quotes in echo?"

The direct answer: double quotes allow variable and command expansion inside ($name, $(date) still work) while suppressing word splitting and globbing; single quotes make every character literal — no expansion of any kind.

Going deeper: demonstrate rather than define — echo "$HOME" prints a path, echo '$HOME' prints the five characters $HOME. Add the backslash as the third quoting mechanism for single characters, and the nesting rule: you cannot escape a single quote inside single quotes, so switch to double quotes for text containing apostrophes.

The details that separate candidates: framing quotes as instructions to the shell's rewriting pipeline (which expansions to skip) rather than "string syntax"; and the practical routing rule — single-quote text destined for programs with their own $ languages (awk, regex) so the shell keeps its hands off. That one habit marks people who have debugged real pipelines.

🎯 "What is the difference between $VARIABLE and ${VARIABLE}?" — asked verbatim at LinuxTeck

The direct answer: identical in meaning; the braces explicitly mark where the variable's name ends. Required when text is glued directly after: ${file}_backup — because $file_backup would be read as one longer variable name.

Going deeper: explain the failure mode precisely — bash swallows the longest legal name (letters, digits, underscores), finds that variable unset, and expands it to nothing, silently. The bug presents as mysteriously empty output, not as an error.

The details that separate candidates: knowing braces are also the doorway to parameter expansion — defaults like ${var:-fallback} and trims like ${file%.txt} (an entire module of this track, Module 9, lives inside those braces) — signals depth beyond the syntax trivia the question literally asks.

🎯 "What are environment variables in shell scripting?" — asked verbatim at PlacementPreparation; LinuxTeck asks "What are environment variables?" (answered fully in Part C)

The direct answer: key-value settings the shell passes down to every program it starts — PATH, HOME, USER are the classics. They are how configuration flows from shell to program without either one reading a file.

Going deeper: the crisp distinction is exported vs not — an ordinary variable is private to the current shell; export marks it for copying into child processes. printenv lists only the exported set.

The details that separate candidates: the direction of flow — children receive copies at start-up, and nothing a child changes flows back up; Part C proves it in two lines, and Module 12 explains the process mechanics underneath.

Part C — Private variables, exported variables

C1. export: which jars the children inherit

Module 1 C1 told you a script runs in its own fresh bash, which "inherits a copy of much of your shell's environment" — and promised this module would say exactly what travels. Here it is: only exported variables travel. A plain color="red" is private to the shell that made it; export color stamps it "copy me into every program this shell starts from now on." The exported set is the environment — the same thing printenv was listing back in Module 1.

Two commands to inspect the two sets: printenv prints only the environment (exported variables); the builtin set prints everything, private and exported alike. When a variable shows in set but not printenv, it exists but will not reach your script — a diagnosis you will make many times in your career.

Counter-intuitive: export copies down, once, at launch — never up, never live. A child process receives a snapshot of the exported jars at the moment it starts. Change the variable in the parent afterwards: running children do not see the update. And nothing a child sets ever flows back to the parent — which you already proved in Module 1 (the cd /tmp script that could not move your terminal). "Why doesn't my script see the variable I set?" (not exported) and "why didn't my script's variable survive?" (children cannot write upward) are the two halves of this one fact.
Real-world analogy — the briefing pack. A manager (your shell) starting a new hire (a child process) hands over a briefing pack — photocopies of exactly the documents stamped "for distribution" (export). Documents without the stamp stay in the manager's drawer. The new hire can scribble on their photocopies all they like; the originals in the drawer never change. And a memo the manager writes after the hire started is not magically inserted into the pack they already carry.

Where the analogy stops working. A new hire can walk back to the manager's desk and ask for a missing document. A process cannot: its environment is fixed at launch, and the only way to get an updated set is to be started again. There is no "refresh my environment" call — which is why, after editing ~/.bashrc, you must source it (Module 1 C1) or open a new terminal.

🧪 Exercise 2.7 — the briefing pack in two lines

The first child comes up empty on purpose.

bash
color="red"
bash -c 'echo "child sees: [$color]"'   # a child bash runs one command — single quotes so OUR shell does not fill the blank first
export color
bash -c 'echo "child sees: [$color]"'
Expected result — click to reveal (contains a deliberate empty result)
javascript
child sees: []
child sees: [red]

What to read out of it: the brackets are there to make emptiness visible — line 1's child expanded $color to nothing, because an unexported variable never entered its briefing pack. After export color, an identical child prints red. Note the single quotes around the child's command — B2's "solid wall" doing real work: with double quotes, your shell would have expanded $color before the child even started, and the experiment would be measuring nothing. (This exercise quietly reuses half the module.)

Now imagine this at 500 hosts. Environment variables are the standard way configuration reaches production software — container images, CI pipelines, and systemd services all inject settings as environment variables (DATABASE_URL, API_KEY, LOG_LEVEL) rather than files. The whole pattern works because of the copy-down-at-launch rule: the orchestrator sets the environment, launches the process, and the process carries its config for life. The classic fleet incident is the other half of the rule: an operator exports a fixed value in their own terminal, the already-running service naturally never sees it, and the "fix" appears not to work — a restart was the missing step.

C2. Guard rails: readonly and unset

Two small tools complete the variable story. readonly name locks a jar: any later assignment is refused. Use it in scripts for values that must not drift once set — a release version, a target directory. unset name destroys the jar entirely — which is different from setting it to empty (name="" leaves an empty jar; unset removes jar and label both; some checks in Module 9 can tell the difference).

🧪 Exercise 2.8 — locking and destroying

The middle command fails on purpose.

bash
release="v2.1.0"
readonly release
release="v2.2.0"     # ← refused
echo "after attempt: $release"
tmp="scratch"
unset tmp
echo "after unset: [$tmp]"
Expected result — click to reveal (contains a deliberate failure)
javascript
bash: release: readonly variable
after attempt: v2.1.0
after unset: []

What to read out of it: the assignment was refused with a clear error — and the old value survived untouched. But look closely at what happened next: the script kept going (the echo still ran). A failed line does not stop bash by default — Module 1's order-slip analogy warned you, and Module 14 is where you get the tools to change it. Last line: after unset, the variable expands to nothing, indistinguishable (for now) from never having existed. One habit from this: in a real script, readonly important constants immediately after setting them — it turns silent corruption into a loud message.

Interview questions — Part C

🎯 "What is the difference between a variable and an environment variable?" — asked verbatim at LinuxTeck; PlacementPreparation asks "What is the difference between local and global variables?"

The direct answer: a plain variable exists only in the shell that created it; an environment variable has been exported, so the shell copies it into every child process it starts. Same syntax to read both — the difference is inheritance.

Going deeper: give the two-line proof (set a variable, bash -c 'echo $var' shows empty; export it, the child sees it) and the inspection pair — set shows all variables, printenv only the environment. Note the copy is taken at child launch: later parent changes do not reach running children, and children can never write back.

The details that separate candidates: connecting it to real operations — containers and CI inject configuration as environment variables precisely because of the copy-at-launch rule, and the "exported it but the running service doesn't see it" incident is resolved by a restart, not a bigger export. Also worth one sentence: "local" has a second, stricter meaning inside functions (local), coming in Module 8.

🎯 "How do you add a directory to your PATH?" — asked verbatim at LinuxTeck

The direct answer: export PATH="$PATH:/new/directory" — expand the current value, glue the new directory on the end, assign it back, exported. Put the line in ~/.bashrc to make it happen in every new shell.

Going deeper: explain each piece, because the interviewer is really testing this module — $PATH inside double quotes expands to the current list; the : is PATH's separator (Module 1 A2); assignment replaces the old jar contents; export keeps it flowing to children. Order matters: appending puts your directory last in the search, prepending (PATH="/new/dir:$PATH") makes it win over system commands — powerful and risky in equal measure.

The details that separate candidates: knowing the change lives only in the current shell until it is in a startup file, and that editing ~/.bashrc changes nothing for already-open terminals until they source it — the C1 snapshot rule, applied. Bonus: prepending user-writable directories to root's PATH is a classic privilege-escalation vector, which is why production images keep PATH minimal and fixed.

🎯 "How do you define and use variables in Bash?" — asked verbatim at Zero To Mastery (their answer folds in readonly and unset)

The direct answer: define with name=value (no spaces), read with "$name", protect with readonly name, remove with unset name. Names are case-sensitive; convention reserves UPPERCASE for exported/system variables.

Going deeper: fold in the lifecycle — variables are per-shell and die with it; export extends them to children; nothing persists to disk unless you write it to a startup file yourself. Show fluency by filling from commands too: today=$(date).

The details that separate candidates: two behaviors most candidates cannot state precisely — a readonly violation errors but does not stop the script by default (the next lines still run), and unset var differs from var="" (destroyed vs empty — detectable with the ${var+set} family once you know parameter expansion). Precision on defaults-and-edge-cases is what the question is actually probing.

Part D — Debugging quoting bugs like an engineer

D1. The argument probe: making the invisible boundaries visible

When a command misbehaves and you suspect quoting, the question to answer is always: how many arguments did the command actually receive, and what was in each? You cannot see that by reading the script — you must probe it. The tool is one we teach right here because it is the probe: printf '[%s]\n' args… prints each argument it receives on its own line, in brackets. (printf is echo's stricter sibling: the first argument is a template — %s means "a value goes here", \n means "end the line" — and the template is reused for every remaining argument, which is exactly the behavior that makes boundaries visible.) A plain echo probe cannot do this job: echo joins its arguments with single spaces, so two arguments and one argument-with-a-space print identically — the very difference you are hunting is erased.

Real-world analogy — counting the parcels. A warehouse dispute over "how many parcels did you send me?" is never settled by re-reading the order letter — it is settled at the loading dock, counting what actually arrived. printf '[%s]\n' is the loading dock: one bracketed line per parcel, contents visible, count indisputable.

Where the analogy stops working. At a real dock you can also see who packed each parcel. The probe cannot tell you which rewriting step produced a boundary (splitting? globbing? it was always two values?) — for that you re-run the probe with the quotes added and compare counts. Two probes, one diff, is the whole method.

🧪 Exercise 2.9 — probe first, then fix

The second command fails on purpose.

bash
cd ~/bash-course/quoting-lab
mkdir -p backup
file="monthly report.txt"
cp $file backup/       # ← cp copies files (cp SOURCE DEST) — this fails, twice
printf '[%s]\n' $file  # probe the unquoted expansion: how many arguments?
printf '[%s]\n' "$file"
cp "$file" backup/     # the reflex fix
ls backup
Expected result — click to reveal (contains a deliberate failure)
javascript
cp: cannot stat 'monthly': No such file or directory
cp: cannot stat 'report.txt': No such file or directory
[monthly]
[report.txt]
[monthly report.txt]
'monthly report.txt'

What to read out of it: cp fails exactly like ls did in B1 — same mechanism, any command. Then the probes settle it beyond argument: unquoted → two bracketed lines, two arguments; quoted → one. That pair of probes is the whole diagnosis, reusable on any mystery: run the probe on the exact expansion your failing command used, count the brackets. The final ls backup confirms the quoted cp landed one file where it belonged — quoted by ls again, as in 2.4.

D2. Quotes inside quotes

Real text contains apostrophes, and sooner or later you must print It's while also expanding a variable. The clean solution needs no new syntax: apostrophes are ordinary characters inside double quotes (B2's table — only $, backticks, \ and " stay special there). So double quotes handle both jobs at once. The trap is starting from single quotes — 'It's' ends the string at the middle apostrophe; there is no escaping a single quote within single quotes (B2's photocopy rule). When text is mostly literal $ signs with an apostrophe too, you can also concatenate styles back-to-back: 'literal part'"dynamic $part" — bash glues adjacent quoted pieces into one word.

🧪 Exercise 2.10 — apostrophes and dollars together
bash
user="Ada"
echo "It's $user's shift today"        # apostrophes are safe inside double quotes
echo 'The price is $5'" for $user"     # two quoting styles, glued into one argument
Expected result — click to reveal
javascript
It's Ada's shift today
The price is $5 for Ada

What to read out of it: line 1 — both apostrophes printed literally while $user expanded; double quotes did both jobs. Line 2 — the single-quoted half protected $5 from expansion (there is no variable named 5 — worse, $5 does mean something in scripts, as Module 7 will show), while the double-quoted half expanded $user; the glue-adjacent-pieces trick produced one seamless argument.

D3. Choosing quotes: the decision tree

Note: the diagram below is a Mermaid code block. Notion does not render it automatically — click the block and switch it from "Code" to "Preview" (or "Split") to see the flowchart.
Diagram source
flowchart TD
    A["I am writing a piece<br>of text in a command"] --> B{"Does it contain<br>a $var or $(cmd)<br>I want expanded?"}
    B -->|"yes"| C{"Any characters that<br>must stay literal?<br>($, apostrophe is fine)"}
    C -->|"no"| D["Double quotes<br>the default reflex"]
    C -->|"a few"| E["Double quotes +<br>backslash each literal:  \$"]
    B -->|"no — exact<br>text only"| F{"Does it contain<br>an apostrophe?"}
    F -->|"no"| G["Single quotes<br>solid wall"]
    F -->|"yes"| H["Double quotes<br>(apostrophes are<br>ordinary there)"]
    D --> I["Never leave $var bare —<br>splitting + globbing await"]
    E --> I

Interview questions — Part D

🎯 "How do you check your current PATH?" — asked verbatim at LinuxTeck

The direct answer: echo "$PATH" — expand the variable, print it. (printenv PATH is the equivalent that names the variable instead of expanding it.)

Going deeper: you now know everything inside that one-liner — PATH is an exported variable (C1), $PATH expands to its value (A1), the double quotes are the B4 reflex. Reading the output: colon-separated directories, searched left to right, first match wins (Module 1 A2).

The details that separate candidates: knowing the subtle difference between the two commands — printenv PATH shows what is actually in the environment, while echo "$PATH" shows the current shell's value, which in exotic cases can differ (a shell variable shadowing an unexported change). Mentioning that distinction, unprompted, signals real debugging miles.

🎯 "How do you print the value of a variable in a script?" — asked verbatim at LinuxTeck

The direct answer: echo "$variablename" — with the quotes, out of habit.

Going deeper: for debugging rather than displaying, prefer the probe from D1 — printf '[%s]\n' "$var" — because it makes emptiness visible ([]), makes trailing spaces visible, and (unquoted, deliberately) reveals how many words the value splits into. echo erases exactly those differences.

The details that separate candidates: volunteering echo's edge cases — a value that is exactly an echo option — -n, -e, -E, or a combination like -en — is swallowed by echo as an option (a -n value prints nothing at all, not even a newline), which is why serious scripts print untrusted values with printf '%s\n' "$var" instead. Candidates who know why printf-over-echo is a style rule, not folklore, stand out.

Part E — Production

E1. 🏭 Production practices

These are the practices in production — how the things this module taught are actually done on real systems:

Every expansion is quoted, no exceptions without a comment. "$var", "$(command)" — production style guides and CI linters (ShellCheck, Module 14) enforce this mechanically; an unquoted expansion in review is treated as a bug even if today's data is safe.

Constants are readonly. Values that must not drift — versions, target paths, environment names — are locked right after assignment: readonly DEPLOY_ENV, turning silent corruption into a loud error.

Configuration enters through exported environment variables. Containers, CI systems, and systemd units inject DATABASE_URL-style settings as environment variables; scripts read them rather than parsing config files, and document which variables they require.

Secrets are exported with care. Environment variables holding credentials are never echoed into logs, and long-lived shells avoid exporting them at all (any child process inherits them — including that debugging tool someone runs later).

Untrusted values are printed with printf, not echo. printf '%s\n' "$var" renders any value faithfully; echo mangles values that look like its own options.

UPPERCASE is reserved for exported/system variables; script-local variables are lowercase. The naming convention is a safety rail — it makes "this value leaves the script" visible at a glance and prevents accidental shadowing of PATH or HOME.

E2. Production-practice table

SymptomWhat is really happeningWhat to runThe fix
Command errors on two files you never mentioned — both halves of one real nameWord splitting: an unquoted $var containing a space became two argumentsprintf '[%s]\n' $var vs printf '[%s]\n' "$var" — compare bracket countsQuote the expansion: "$var" — then audit the script for its unquoted siblings
Output mysteriously empty; no error anywhereAn unset variable expanded to nothing — typo, wrong case, or a swallowed name like $file_backupprintf '[%s]\n' "$var" (shows []), and check spelling/braces at the use siteFix the name or add braces ${file}_backup; adopt set -u (Module 14) to make this loud
A variable's value came out as a list of filenamesGlobbing: unquoted expansion contained * or ?, and bash matched it against the directoryecho $var vs echo "$var" — if outputs differ, the value is being treated as a patternQuote the expansion; treat any data that can contain * as radioactive until quoted
Script cannot see a variable that is clearly set in your terminalThe variable was never exported — private jars do not enter the child's briefing packset | grep -i '^name=' vs printenv name — present in first, absent in secondexport name before launching the script — or pass it inline: name=value ./script.sh
Exported the fix, but the running service still uses the old valueEnvironment is copied at launch; running processes never receive updatesCheck the service's actual environment: tr '\0' '\n' </proc/PID/environ (Linux)Restart the process after changing its environment — there is no live refresh
$name printed literally in the output instead of its valueSingle quotes (or an escaped \$) suppressed expansion where you wanted itLook at the quoting on the failing line; single-quoted sections never expandSwitch that section to double quotes — expansion on, splitting still off

E3. 🎓 Capstone — four tickets from the queue

Work each ticket yourself before opening the answer. Everything needed was taught in Modules 1–2.
🎓 Ticket 1 — "Our log-archiver worked for months. Today it errored 400 times: cannot stat 'app' / cannot stat 'server (staging).log' — but no such files exist, and nobody changed the script."

Diagnosis. Two errors, and glued together they spell one real filename: app server (staging).log. That is word splitting's signature (B1): somewhere in the script an unquoted $f met the fleet's first filename containing spaces. Nobody changed the script — the data changed, which is exactly how quoting bugs stay dormant.

Work the steps: find the expansion the failing command used and probe it: printf '[%s]\n' $f → two-plus brackets confirms. printf '[%s]\n' "$f" → one bracket, the real name.

Fix and prevention: quote that expansion — then, because unquoted habits never come alone, audit the whole script for bare $ uses and quote them all (the module's rule: fix the class, not the instance). Add ShellCheck to CI (Module 14) so the next dormant one is caught at review time instead of at 400-errors time.

🎓 Ticket 2 — "My deploy script prints Deploying to cluster — with the environment name just… missing. The variable is right there at the top of the script: deploy_env=staging, and later: echo \"Deploying to $deployenv cluster\"."

Diagnosis. The silent-emptiness family (A1/A2): the script sets deploy_env but reads $deployenv — a different, never-set variable. Bash expanded it to nothing without a word of complaint. (The doubled space in the output is the fingerprint: the spaces around the vanished value survived.)

Work the steps: printf '[%s]\n' "$deployenv"[]. Compare spellings character by character — underscores are the usual culprit, capitalization second.

Fix and prevention: correct the name. Then make this whole bug class impossible: set -u at the top of the script (previewed here, taught in Module 14) makes any unset expansion a fatal error, and readonly deploy_env right after assignment guards the other direction (accidental overwrite). One more habit that would have caught it: probing with brackets during development instead of eyeballing echo output.

🎓 Ticket 3 — "Security flagged our runbook one-liner: it was supposed to log the literal pattern $user for auditors, but the log shows real usernames. Line: echo \"audit pattern: $user\" >> audit.log."

Diagnosis. B2 in reverse: the runbook wanted no expansion, and double quotes are see-through — $user expanded to whatever the variable held in the operator's shell. Text meant to stay literal belongs in single quotes.

Work the steps: reproduce safely: user="realname"; echo "audit pattern: $user" → expands; echo 'audit pattern: $user' → literal. Confirm which the auditors need (the literal pattern).

Fix and prevention: single-quote the literal text: echo 'audit pattern: $user' >> audit.log. Where a line must mix a literal $ with real expansions, use the glue trick (D2) or escape just that one: "\$user". The reviewable rule for runbooks: any $ inside double quotes will expand — if you see one that must not, the quoting is wrong even if today's shell happens to have the variable unset.

🎓 Ticket 4 — "A teammate exported API_URL in their terminal, verified it with echo, then ran ./smoke-test.sh — and the script used the old URL from somewhere. Ran it again: same. 'Export is broken on this box.'"

Diagnosis. Export is never "broken" — something else satisfies the script first. Two prime suspects, both from this module: (1) the script itself assigns API_URL= internally, overwriting the inherited copy (the child may do as it likes with its photocopies — C1); or (2) the teammate exported in one terminal and ran the script in another shell — separate shelves (A1).

Work the steps: in the launching terminal: printenv API_URL — confirms it really is in the environment (if set shows it but printenv does not, the export never happened). Then grep -n "API_URL=" smoke-test.sh — an internal assignment wins over the inherited value.

Fix and prevention: if the script overwrites, change its line to honor the environment with a default — API_URL="${API_URL:-https://default.example}" (the ${:-} default syntax gets full treatment in Module 9; here, read it as "keep the inherited value if present"). Production convention: scripts document the environment variables they accept and never blindly reassign them.

E4. Documentation reference

TopicAuthoritative referenceWhat you'll find there
Variables and assignmentBash manual — Shell ParametersExact assignment syntax and how variables are stored
The rewriting pipelineBash manual — Shell ExpansionsAll seven expansions and their fixed order — the map this module walked
QuotingBash manual — QuotingBackslash, single quotes, double quotes — the exact rules for each
Word splittingBash manual — Word SplittingThe IFS mechanism behind B1, in the manual's own words
GlobbingBash manual — Filename Expansion*, ?, […] patterns and the options that control them
Command substitutionBash manual — Command Substitution$( ) vs backticks, nesting, trailing-newline trimming
export, readonly, unsetBash manual — Bourne Shell BuiltinsPrecise semantics of the three variable-lifecycle builtins
The environmentenv(1) — man7.orgInspecting and modifying the environment handed to a single command

E5. Self-assessment

Answer from memory, out loud or on paper. Every answer is in this module.

  1. Why must name="Ada" have no spaces around the = — what does bash do with name = "Ada" instead?
  2. What does an unset variable expand to, and why is that more dangerous than an error?
  3. When do you need ${file} instead of $file? What exactly goes wrong without the braces?
  4. Describe what bash does with ls -l $file when file="monthly report.txt" — step by step, expansion then splitting.
  5. Double quotes vs single quotes: which expansions survive inside each? Give the one-line demo with $HOME.
  6. A variable's value is *. What is the difference between echo $msg and echo "$msg", and which rewriting step causes it?
  7. What is the argument probe, and why can echo not do its job?
  8. What exactly does export change about a variable? When is the environment copied to a child, and what never flows back?
  9. Your terminal has TOKEN set but your script sees nothing. Which two commands tell you whether it is an export problem?
  10. What is the difference between unset var and var=""?
  11. What does readonly protect against, and does a violated readonly stop the script?
  12. State the quoting reflex in one sentence, and name the two legitimate reasons to break it (even if you can't demonstrate them yet).

E6. Sources

Interview questions in this module were captured verbatim from:

LinuxTeck — Top 70 Shell Scripting Environment Setup Interview Questions — "What are environment variables?", "What is the difference between a variable and an environment variable?", "How do you print the value of a variable in a script?", "What is the difference between single and double quotes in echo?", "How do you create a variable in a script?", "What is the difference between $VARIABLE and ${VARIABLE}?", "How do you check your current PATH?", "How do you add a directory to your PATH?" (page updated June 25, 2026)

Edureka — Top 60 Shell Scripting Interview Questions and Answers — "What are the different types of variables used in Shell Script?", "Differentiate between ' and " quotes." (page updated Dec 9, 2024)

InterviewBit — Top Shell Scripting Interview Questions — "What do you mean by Shell variable?", "What are different types of variables mostly used in shell scripting?" (no publication date shown on page)

PlacementPreparation — Top 50 Shell Scripting Interview Questions for Freshers — "What are shell variables?", "What is command substitution in shell scripting?", "What are environment variables in shell scripting?", "What is the difference between local and global variables?" (published Sept 23, 2024; last updated Feb 27, 2025)

Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "How do you define and use variables in Bash?", "What is command substitution in Bash?" (published June 18, 2026)

A note on the corpus: published interview questions cover variables, quoting, and command substitution densely, but word splitting is rarely asked about by name — it hides inside quoting questions. This module teaches it explicitly anyway, because it is the mechanism those questions are really about; no questions were invented to fill the gap. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2; machine-dependent values (dates, hostnames, usernames) are marked where they occur.

Next: your scripts can now remember values — but they cannot yet react. Every command finishes with a hidden verdict, success or failure, and reading that verdict is how scripts make decisions and how CI pipelines know when to stop. That is Module 3 — Exit Codes and Command Chaining.
Spotted a mistake or want something added? Send me a note.