Module 8 — Functions and Script Structure
Updated 3 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
Part A — Defining and calling
A1. A function is a named block, defined before it is called
A function gives a block of commands a name; saying the name later runs the block. The standard form:
log() {
echo "[$(date +%H:%M:%S)] $*"
}
log "deploy starting"The parentheses are empty punctuation marking "this is a function definition" — arguments do not go there (A2 shows where they go). The body sits in { }. An alternative spelling function log { … } exists in bash; the name() form is the portable convention, and mixing both in one file is a style smell.
One rule follows from bash's nature as a top-to-bottom reader (Module 1): a function must be defined earlier in the file than its first call. There is no hoisting, no lookahead — a call above the definition is just an unknown command (exit 127, Module 3's old friend). Hence the universal layout: functions at the top, action at the bottom — C1 makes that a formal template.
Where the analogy stops working. A team remembers plays from last season. Bash remembers nothing across runs: definitions live only in the shell that read them, die with it (Module 2's jars), and must appear in every script that uses them — which is why shared functions get a file of their own that scripts source (C2), the closest bash has to a playbook binder.
🧪 Exercise 8.1 — name a play
cd ~/bash-course
nano fn1.sh # six lines:#!/bin/bash
log() {
echo "[$(date +%H:%M:%S)] $*"
}
log "deploy starting"
log "deploy finished"chmod +x fn1.sh
./fn1.sh✅ Expected result — click to reveal
[09:00:24] deploy starting
[09:00:24] deploy finishedWhat to read out of it (your timestamps will differ): two calls, one definition — the timestamp logic exists in exactly one place, so changing the log format is a one-line edit forever after. Note the body is pure Module-2 machinery: $(date +%H:%M:%S) is command substitution with a format argument, $* is Module 7's weld — correct here, because a log line is exactly the "one display string" case. Functions add no new expansion rules; they package the ones you know.
A2. Function arguments: $1 again, wearing a different hat
Arguments go after the function's name at the call, exactly like arguments to a script — and inside the function, they arrive as $1, $2, $#, "$@": the same positional machinery, temporarily re-pointed. For the duration of the call, the function's arguments shadow the script's; when it returns, the script's own $1 is back, untouched. Everything Module 7 taught applies wholesale — quoting, $# guards, shift, even getopts (with the OPTIND reset from Module 7's E2).
🧪 Exercise 8.2 — arguments in, arguments restored
greet() { echo "Hello, $1 (of $# args)"; }
greet alice
greet "Ada Lovelace" extra✅ Expected result — click to reveal
Hello, alice (of 1 args)
Hello, Ada Lovelace (of 2 args)What to read out of it: each call got its own private $1/$# — one argument, then two, the quoted name arriving whole (Module 7's rules, unbroken). The one thing that does not get re-pointed: $0 still names the script, not the function (a function's own name lives in FUNCNAME, for the rare moment you need it). One habit transfers immediately: functions validate their arguments exactly like scripts do — [ "$#" -eq 1 ] || return 2 is A2 of Module 7, miniaturized.
Interview questions — Part A
🎯 "How to write a function?" — asked verbatim at Edureka; Zero To Mastery asks "How do you use functions in Bash?"; Hirist asks "How do you define and call a function in Bash?"
The direct answer: define with name() { commands; }, call by bare name with arguments after it: name arg1 arg2. Inside, arguments are $1, $2, $#, "$@" — the positional-parameter machinery, scoped to the call.
Going deeper: the definition-before-call rule (bash reads top to bottom; no hoisting), the two definition syntaxes and why the portable name() form wins, and the fact that a function call is cheap — no new process, unlike running a script (which is why cd inside a function does affect the caller, unlike cd in a child script — Module 1's distinction, inverted, and a great interview moment).
The details that separate candidates: naming that in-process property explicitly — functions can change the calling shell's state, scripts cannot — and its corollary that function-heavy code needs local discipline (Part B); plus knowing declare -f name prints a function's current definition, the fastest way to inspect what is actually loaded in a live shell.
Part B — Scope and results
B1. Variables in functions are global — unless you say local
Here is the scoping fact that surprises everyone arriving from other languages: a variable assigned inside a bash function is the same global variable as everywhere else. No wall, no privacy — a function that casually uses i, tmp, or count overwrites the caller's i, tmp, count. The fix is one keyword: local name (or local name=value) makes the variable exist only for the duration of the call, restoring whatever was there before when the function returns.
Where the analogy stops working. Bash's local is dynamic, not lexical: your notepad is visible not just to you but to any function you call while holding it — callees see (and can modify!) their caller's locals of the same name. It rarely bites, but when interviewers push on "is local like other languages' local?", the precise answer is no: it is call-stack-scoped, not block-scoped.
🧪 Exercise 8.3 — corrupt, then contain
The first function corrupts on purpose.
counter=10
bump() { counter=99; } # no local — writes the corridor board
bump
echo "after bump: $counter"
safe() { local counter=0; counter=5; echo "inside safe: $counter"; }
safe
echo "after safe: $counter"✅ Expected result — click to reveal (contains deliberate corruption)
after bump: 99
inside safe: 5
after safe: 99What to read out of it, carefully: bump overwrote the script's counter — 10 became 99 with no ceremony. safe then worked on its own local counter (watch it hold 5 inside) — and the final line shows the corridor board still reads 99: safe's notepad was tossed, and bump's earlier graffiti is what remains. The exercise's sting is that last line: local protected the world from safe, but nothing un-writes what bump already did. Scope discipline works prospectively only.
B2. What a function gives back: verdicts, and output
Functions produce results through the same two channels every command uses (Modules 3–4) — and confusing them is the classic mistake. Channel one, the verdict: a function's exit status is its last command's, or whatever return N says explicitly. That makes well-named functions read as English in conditionals — is_even() { [ $(( $1 % 2 )) -eq 0 ]; } and then if is_even "$n". return is for numbers 0–255 meaning success/failure — it is not how you send a value back. Channel two, output: a function that computes something prints it, and the caller captures with command substitution: stamp=$(now_stamp) — exactly how you capture any command, because a function call is a command.
And the sharp edge that pays off a Module 3 debt: return ends a function; exit ends the whole script — from anywhere, including inside a function. A helper that "handles" an error with exit doesn't return control to its caller; it vaporizes the caller. Libraries of functions (C2) must use return exclusively and let the top level decide about exiting.
🧪 Exercise 8.4 — the two channels, and the grenade
The last function kills the script on purpose.
cd ~/bash-course
nano fn2.sh # this script:#!/bin/bash
is_even() { [ $(( $1 % 2 )) -eq 0 ]; }
now_stamp() { date +%Y%m%d; }
f() { return 3; echo "never printed"; }
g() { exit 5; }
is_even 4 && echo "4 is even"
is_even 7 || echo "7 is odd"
backup="db-$(now_stamp).sql"
echo "would create: $backup"
f; echo "after f: $?"
echo "before g"
g
echo "you will not see this"chmod +x fn2.sh
./fn2.sh ; echo "script verdict: $?"✅ Expected result — click to reveal (contains a deliberate script-kill)
4 is even
7 is odd
would create: db-20260903.sql
after f: 3
before g
script verdict: 5What to read out of it (your date will differ): is_even never echoed anything — its entire interface is the verdict, which &&/|| and if consume directly. now_stamp's interface is output, captured into a filename. f shows return's two jobs at once: it stopped the function (the echo after it never ran) and set the verdict (3, read by the caller). Then the grenade: g's exit 5 ended not g but the script — the final echo is missing, and the script's own verdict became 5. In a 300-line script, an exit buried in a helper produces exactly this: a run that just… stops, with the log's last line nowhere near the real cause.
Interview questions — Part B
🎯 "How do you return a value from a bash function?" — the classic trap question; asked in many phrasings (no canonical published wording — tutorial answers to Zero To Mastery's function question cover it), and designed to catch a specific misconception
The direct answer: you don't — not the way other languages mean it. return N sets only an exit status, 0–255, meaning success/failure. Values come back as output: the function prints, the caller captures with result=$(fn args).
Going deeper: show both channels working together — fn prints its answer and returns a status, the caller does out=$(fn x) || fallback — and name the trap in the question: return "hello" errors, return 300 wraps to 44 (Module 3's one-byte fact). Also worth stating: echo-as-return means a function's diagnostics must go to stderr (>&2, Module 4), or they contaminate the captured value.
The details that separate candidates: that $(fn) runs the function in a subshell — its variable assignments don't reach the caller (Module 12's mechanics, arriving early); so the three honest value-passing options are stdout-capture, a global the function documents it sets, or (bash 4.3+) a nameref — naming all three, with stdout-capture as the default recommendation, is a complete answer.
🎯 "What is the difference between local and global variables in a bash function?" — a standard screen; published lists cover local/global for scripts (PlacementPreparation's "local vs global" question) more than for functions specifically
The direct answer: nothing is local unless declared — assignments inside functions hit the same global namespace as everywhere else. local var scopes the variable to the current function call, restoring the previous value on return.
Going deeper: give the corruption demo (a helper assigning counter overwrites the caller's) and the discipline (every function-internal variable gets local, first mention). Then the precision point: bash's local is dynamic scope — visible to callees during the call — not the lexical scope of most languages.
The details that separate candidates: the local + command substitution gotcha — local out=$(cmd) masks cmd's exit status (local's own success wins); split it: local out; out=$(cmd) || … — a real production bug with a two-line fix, and ShellCheck flags it (SC2155). Candidates who know that one have debugged real scripts.
Part C — Script structure: the shape production scripts share
C1. The template
Once functions exist, nearly every serious bash script converges on one layout — worth internalizing as a template you fill rather than a structure you invent each time:
#!/bin/bash
# deploy.sh — one-line purpose statement
# usage: deploy.sh -e env [-n] target
# (strict-mode flags from Module 14 will land here)
readonly SCRIPT_NAME="${0##*/}" # constants first, locked (Module 2)
readonly DEPLOY_ROOT="/opt/deploys"
log() { echo "[$SCRIPT_NAME] $*" >&2; } # helpers: small, local-disciplined
die() { log "ERROR: $*"; exit 1; } # the ONE place a helper may exit
usage() { echo "usage: $SCRIPT_NAME -e env [-n] target" >&2; exit 2; }
main() {
[ "$#" -ge 1 ] || usage # validation (Module 7)
local target="$1"
log "deploying $target"
# real work, calling helpers, checking verdicts
}
main "$@" # the only top-level action lineWhy this shape wins: the file reads top-down as declaration → tools → intent; the only executable line at top level is main "$@", so nothing runs while the file is being read as a library; main's local variables keep the global namespace clean; and die concentrates the exit-from-helper decision (B2's grenade) into one audited place. The main "$@" forwarding is Module 7's "$@" doing its precise job — the script's arguments arrive in main's $1, $2 untouched.
Where the analogy stops working. A kitchen's stations operate in parallel; this template is still strictly sequential (bash reads top to bottom, runs main last). The template's win is not concurrency but addressability: every behavior has one named home, so the 3 a.m. fix goes into a function, not into "somewhere around line 180."
🧪 Exercise 8.5 — feel the difference structure makes
No new commands — an editing exercise. Take your opts.sh from Module 7 and restructure it into the template: constants up top, the getopts loop inside main, error text through a die helper. Then run its happy path and both failure paths again (Exercise 7.4's four runs) and confirm identical behavior.
✅ Expected result — click to reveal
env=staging dry=1 leftover=release-42
unknown option: -x
verdict: 2
option -e needs a value
verdict: 2
usage: ./opts.sh -e environment [-n]What to read out of it: byte-for-byte the same behavior as Module 7's flat version — restructuring is behavior-preserving, and proving that with the old test runs is the habit that matters (you just performed your first refactor with a regression test, which is what Module 15's bats testing industrializes). The payoff is invisible in output and enormous in the diff: the next feature lands as one new function plus one call line.
C2. Libraries: source, and the return-not-exit contract
When several scripts want the same helpers, the functions move to a shared file and each script loads it: source /opt/lib/common.sh (Module 1 C1's source, now with its real production purpose: sourcing a file of definitions loads them into the current shell). Two contracts make a file library-safe. First: definitions only — a library that does things at top level does them at every source, in every consumer, at load time. Second: helpers return, never exit (B2's grenade rule, now structural): sourced code runs in the consumer's shell, so a library's exit kills the consumer — and Module 3's interview answer about exit in sourced files is now something you can derive rather than memorize.
🧪 Exercise 8.6 — build a library, and prove the contract matters
cd ~/bash-course
nano common.sh # a LIBRARY — no shebang needed, nothing executes at top level:# common.sh — shared helpers (source me; do not execute)
log() { echo "[$(date +%H:%M:%S)] $*" >&2; }
require_file() {
[ -f "$1" ] || { log "missing required file: $1"; return 1; }
}nano uses-lib.sh # a consumer:#!/bin/bash
source ./common.sh
log "starting checks"
require_file /etc/hosts && log "hosts: ok"
require_file /etc/nothere || log "continuing without optional file"
log "still alive — the library returned instead of exiting"chmod +x uses-lib.sh
./uses-lib.sh✅ Expected result — click to reveal
[09:15:02] starting checks
[09:15:02] hosts: ok
[09:15:02] missing required file: /etc/nothere
[09:15:02] continuing without optional file
[09:15:02] still alive — the library returned instead of exitingWhat to read out of it (timestamps yours): one library, one consumer, and the contract visibly holding — require_file's failure came back as a verdict (caught by ||, Module 3), not as a script-ending explosion; the consumer decided the missing file was survivable and said so. Mentally swap the library's return 1 for exit 1 and re-read the transcript: everything after line 3 vanishes. That is the whole difference between a library and a landmine. (Also note every log line went to stderr — >&2 in one place, Module 4's discipline inherited by every consumer for free.)
Part D — The map
D1. Where a result should travel — the decision
Diagram source
flowchart TD
A["My function produces<br>something for the caller"] --> B{"What is it?"}
B -->|"success / failure<br>(maybe which failure)"| C["exit status:<br>last command, or return N"]
C --> C1["caller: if fn / fn || ...<br>rc=$? for the number"]
B -->|"a computed value<br>(string, path, count)"| D["print to stdout"]
D --> D1["caller: out=$(fn args)<br>diagnostics must go >&2"]
B -->|"several values /<br>complex state"| E["document a global<br>the function sets"]
E --> E1["name it loudly:<br>FN_RESULT_ARRAY etc."]
B -->|"'stop everything'"| F{"am I a library<br>or the top level?"}
F -->|"library"| G["never exit —<br>return nonzero, let<br>the caller decide"]
F -->|"top level / die()"| H["exit N — once,<br>audited, logged"]Interview questions — Parts C–D
🎯 "What is the difference between source and executing a script directly?" — asked verbatim at Hirist; Zero To Mastery's variant appeared in Module 1 — here is the function-era layer of the same question
The direct answer: executing runs in a child shell (changes die with it); sourcing runs in the current shell (changes persist). Module 1 proved it with cd; the layer that matters now: sourcing is how function libraries load — definitions are changes to the shell, so they only stick if sourced.
Going deeper: the two library contracts (definitions-only at top level; return-not-exit) and why each follows from sourcing's mechanics — top-level code runs at load, in the consumer; an exit is the consumer's exit. Mention the guard some libraries carry: refusing to run when executed directly (comparing ${BASH_SOURCE[0]} to $0 — the library-vs-script self-test).
The details that separate candidates: knowing source searches PATH for relative names (surprising!) so libraries are sourced by explicit path (source "$(dirname "$0")/common.sh" is the classic spelling); and the operational point that sourcing user-supplied files is code execution in your shell — a security boundary, not a convenience.
🎯 "Why does my script die halfway with no error — and the last log line is from a helper function?" — the production-shaped version of return-vs-exit; interviewers pose it as a debugging scenario rather than a definition
The direct answer: some helper called exit — which ends the entire script from any depth — where it should have used return. The script didn't crash; it was told to stop, by code that thought it was being tidy.
Going deeper: the audit is mechanical — grep -n 'exit' script.sh lib/*.sh and justify every hit; the fix is structural — helpers return verdicts, one die() at the top level owns exiting (C1's template), libraries never exit (C2's contract). The $? of the vanished script equals the helper's exit argument — a forensic clue worth naming.
The details that separate candidates: the subshell nuance — exit inside $( ) or a pipeline stage kills only that subshell (Module 12), so the same keyword is fatal in one position and harmless in another, and candidates who can say which positions and why have genuinely internalized the process model; plus trap ... EXIT (Module 13) as the way to make even surprise exits leave a forensic last word.
Part E — Production
E1. 🏭 Production practices
Scripts follow the template. Header comment with purpose and usage; constants readonly at the top; functions next; a single main "$@" at the bottom. Reviewers reject top-level action lines scattered through a file the way they reject unquoted expansions.
Every function-internal variable is local, declared at first mention. And declared separately from command substitutions — local out; out=$(cmd) — so the substitution's verdict survives (SC2155 is in the CI linter for exactly this).
Functions expose one interface: verdict, stdout value, or documented global — chosen deliberately. Diagnostics always on stderr so captured values stay clean.
exit lives in exactly one helper (die) plus the top level; everything else returns. Libraries never exit, never act at load time, and are sourced by explicit path with versions pinned like any dependency.
Shared helpers live in a sourced common.sh, owned, reviewed, and changed like production code — because it is production code running inside every consumer.
Functions are the unit of testing. Because the template keeps logic in functions and action in main, a test harness (bats, Module 15) can source the script and exercise functions individually — structure now is testability later.
E2. Production-practice table
| Symptom | What is really happening | What to run | The fix |
|---|---|---|---|
| Caller's loop counter jumps or its variables change after calling a helper | The helper assigned without local — same global namespace | declare -f helper and read its assignments; probe the variable before/after the call | local on every function-internal variable, first mention |
| command not found (127) for a function you can see right there in the file | The call sits above the definition — bash had not read it yet | Note the line numbers of call vs definition | Template layout: all functions first, main "$@" last |
| A captured value contains log lines: out = "starting… 42" | The function's diagnostics went to stdout, and $( ) captured everything printed | fn >/dev/null — what disappears was on stdout; what remains was correct stderr | Diagnostics to >&2; stdout reserved for the value |
| Script stops mid-run, no error; $? is some small number; last log line is from a helper | A helper called exit instead of return — the B2 grenade | grep -n 'exit' script.sh lib/*.sh and justify each hit | Helpers return; one die() owns exiting; libraries never exit |
| local out=$(cmd) never detects cmd's failure | local's own success (0) overwrites the substitution's verdict | Reproduce: f(){ local o=$(false); echo $?; }; f → 0 | Split: local out; out=$(cmd) || handle (ShellCheck SC2155) |
| Sourcing the library ran actions — restarted something, printed output | The library has top-level action lines; source executes them at load | Read the library for anything outside a function definition | Definitions-only contract; move actions into functions callers invoke deliberately |
E3. 🎓 Capstone — four tickets from the queue
🎓 Ticket 1 — "After we added a retry() helper to the deploy script, the main loop deploys to the first three hosts, then starts over from host one, forever. The loop is for ((i=0; i<${#hosts}; i++))-style with a counter i — untouched for months."
Diagnosis. B1's innocent-variable corruption: retry() uses i for its attempt counter, without local. Every retry call resets the caller's i, rewinding the host loop — the bug manifests in the loop that did not change, which is exactly why this class is so hard to see (the yellow callout's warning, live).
Work the steps: declare -f retry and read for assignments; then instrument: print i before and after one retry call — watch it change hands.
Fix and prevention: local i (and every other variable) inside retry; then sweep the whole library — corruption rarely travels alone: grep -n '=' lib/common.sh inside function bodies, demanding local on each. Team rule going forward: helpers declare all their variables local in the first lines, where review can check at a glance.
🎓 Ticket 2 — "Our get_active_region() helper returns the region — the deploy uses get_active_region; region=$?. It worked in us-east-1 for a year. This week traffic moved to eu-central-1 and the deploy started targeting region '3'."
Diagnosis. B2's channel confusion, aged into production: someone encoded regions as return numbers (us-east-1 → some index that happened to work as long as nothing changed), and $? faithfully delivered a small integer — statuses are 0–255 numbers, never strings, so the eu move surfaced as a meaningless "3". The function's interface was wrong from birth; stability hid it.
Work the steps: declare -f get_active_region — find the return $index; trace where "3" entered the target list.
Fix and prevention: flip the channel: the helper prints the region name, callers capture — region=$(get_active_region) || die "cannot determine region" — with diagnostics on stderr so the capture stays clean. Sweep for siblings: any $? being used as data rather than verdict is the same disease. Interface rule for the library README: verdict = did it work; stdout = what it found.
🎓 Ticket 3 — "We sourced the new common.sh into our maintenance script. Now, whenever a preflight check fails on ONE host, the whole orchestrator — which loops over 200 hosts — vanishes mid-run. No stack trace, nothing."
Diagnosis. C2's contract violation: the library's preflight_check() "handles" failure with exit 1. Sourced code runs in the consumer's shell, so that exit is the orchestrator's exit — one bad host vaporizes the round (and Module 6's fleet rules said a round must survive per-item failure). The missing stack trace is the fingerprint: nothing crashed; something chose to stop.
Work the steps: grep -n 'exit' common.sh — every hit inside a function is a suspect; reproduce with one host and watch echo $? after the vanish (it equals the library's exit code).
Fix and prevention: library-wide: exit → return (with meaningful codes); orchestrator decides per-host policy: preflight_check "$host" || { log "skip $host"; continue; }. Add the C2 guard to the library so executing it directly still works for testing, and add a review checklist line: a library that can end its consumer is not a library.
🎓 Ticket 4 — "A teammate 'cleaned up' a working script into functions. Now running it does… nothing at all. No output, no error, exit 0. The functions are all there — we can see them in the file."
Diagnosis. The template's last line is missing: everything became definitions, and nothing calls them. Bash read the file top to bottom, learned some plays (A1's analogy), reached the end, and exited 0 — a flawless performance of zero requests. It is the gentlest possible failure and a rite of passage.
Work the steps: run with tracing later (Module 15's set -x would show only definitions), or simply: grep -vE '^\s*(#|$)' script.sh and look for any line that is not inside a function — there is none.
Fix and prevention: append the missing dispatcher: main "$@". Then upgrade the refactor to the full template while you are there (constants readonly, local audit, die helper) and rerun the old invocation tests (Exercise 8.5's habit) — a refactor is not done until the old behavior is demonstrated back. The one-line moral for the runbook: a script of pure functions is a library; main "$@" is what makes it a program.
E4. Documentation reference
| Topic | Authoritative reference | What you'll find there |
|---|---|---|
| Function syntax and semantics | Bash manual — Shell Functions | Both definition forms, argument handling, FUNCNAME, dynamic scoping of local |
| return and source | Bash manual — Bourne Shell Builtins | return's exact rules (functions and sourced files); source's PATH behavior |
| local and declare | Bash manual — Bash Builtins | local's options; declare -f for inspecting loaded functions |
| The SC2155 gotcha | ShellCheck SC2155 | Why local out=$(cmd) masks cmd's exit status, with the split-declaration fix |
E5. Self-assessment
Answer from memory, out loud or on paper. Every answer is in this module.
- Write the standard function definition syntax, and state the one ordering rule bash enforces about definitions and calls.
- Inside a function, what do $1, $#, "$@" refer to? What happens to the script's own $1 during the call, and what does $0 hold?
- Why is an undisciplined helper's tmp=… assignment dangerous? What is the one-keyword fix and the team rule around it?
- In what precise way is bash's local not like other languages' local variables?
- A function must hand back a computed string. Name the wrong way (and why it fails beyond 255), and write the right way — both sides, function and caller.
- What are return's two simultaneous effects? What does exit inside a function do instead?
- Why does local out=$(cmd) hide cmd's failure, and what is the two-line spelling that doesn't?
- Sketch the production script template's five layers, and explain why main "$@" is the only top-level action line.
- State the two contracts of a function library, and derive each from how source works.
- Why do libraries source by explicit path rather than bare name?
- A script stops mid-run with no error and $? = 4. Reconstruct the likely cause and the grep that finds it.
- Why does the captured output of a well-written function contain no log lines — which two-module combination guarantees it?
E6. Sources
Edureka — Top 60 Shell Scripting Interview Questions and Answers — "How to write a function?" (page updated Dec 9, 2024)
Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "How do you use functions in Bash?" (published June 18, 2026)
Hirist — Top 30+ Shell Scripting Interview Questions and Answers — "How do you define and call a function in Bash?", "What is the difference between source and executing a script directly?" (published Jul 22, 2025; last modified Dec 31, 2025)
A note on the corpus: basic function syntax is well represented; the topics this module leans hardest on — local discipline, return-vs-exit, and library contracts — are asked in real screens as scenario questions ("why did the caller's variable change?", "why did the script vanish?") rather than as published one-liners, and the two scenario questions above are labeled accordingly rather than given invented citations. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2; timestamps and dates are flagged as machine-dependent where they occur.