Module 5 — Conditionals: if, test, and case
Updated 3 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
Part A — if: a verdict reader with a body
A1. if runs a command and reads its exit code — that is the whole trick
Here is the fact that makes bash conditionals click, and that most tutorials bury: if does not evaluate a "condition." It runs a command and reads its exit status. Verdict 0 → the then block runs; any other verdict → it doesn't. The full grammar:
if some-command; then
commands-for-success
elif other-command; then # optional, as many as you like
commands-if-THAT-succeeded
else # optional
commands-if-everything-failed
fi # "if" backwards — closes the blockAny command can stand guard. if grep -q bob users.txt — grep's verdict decides. if ./preflight.sh — your own script's exit code (Module 3 C2) decides; that is why truthful verdicts mattered. And ! in front of a command flips its verdict: if ! grep -q zoe users.txt reads "if zoe is not in the file." Compare this with Module 3's &&: same verdict-reading machinery, but if binds its else to the one command you name — which is exactly the fix for the a && b || c trap.
Where the analogy stops working. A bouncer evaluates you; if evaluates nothing — it delegates entirely. There is no "condition language" hiding inside if; the [ ... ] you have seen in other people's scripts is not if-syntax, it is a separate command with its own verdict, and Part B introduces it as exactly that. Grasping this dissolves half of bash's apparent weirdness in advance.
🧪 Exercise 5.1 — commands as conditions
cd ~/bash-course
if grep -q bob users.txt; then
echo "bob is on the list"
fi
if ! grep -q zoe users.txt; then
echo "zoe is not"
fi
if grep -q zoe users.txt; then
echo "zoe found"
else
echo "no zoe; adding a placeholder note" >&2
fi✅ Expected result — click to reveal
bob is on the list
zoe is not
no zoe; adding a placeholder noteWhat to read out of it: three shapes of the same machine. Shape 1: verdict 0 → then-block. Shape 2: ! flipped grep's 1 into success — "absence" became a positive condition. Shape 3: a real else-branch, bound unambiguously to the one grep named after if — no a && b || c ambiguity possible. Note the else-branch's message goes out >&2 (Module 4 B2): it is commentary, not results. These three shapes, plus Part B's test command, cover the vast majority of all conditionals you will ever read.
Interview questions — Part A
🎯 "How do you use conditional statements in Bash?" — asked verbatim at Zero To Mastery; KnowledgeHut asks "How do you use conditional statements (e.g., if, case) in a shell script?"
The direct answer: if command; then … elif …; then … else … fi — the branch taken depends on each guard command's exit status, with 0 selecting the then-block. case (Part D) handles one-value-many-patterns branching.
Going deeper: lead with the mechanism — if runs a command, not a condition; [ and [[ are just commands whose job is producing verdicts. That framing explains everything else: why any command works as a guard, why if ./deploy.sh is idiomatic, why ! negates.
The details that separate candidates: demonstrating a guard that is not a bracket (if grep -q…, if ping -c1 host) — interviewers specifically probe whether you think brackets are mandatory; and contrasting if/else with && … || one-liners, including why the one-liner is not a safe else (Module 3's trap, cited by name).
Part B — test, alias [: the verdict factory
B1. [ is a command. Its name is one square bracket. Really.
if reads verdicts, but plenty of questions have no ready-made command: is this string empty? is that a directory? is x greater than 3? The answer is a command whose entire purpose is turning small questions into exit codes: test. test -f /etc/hosts exits 0 if that file exists and is a regular file; 1 if not. And test has a famous disguise: the command [ — same program, but when invoked by its bracket name it demands a matching ] as its final argument, purely so your code reads like syntax:
if [ -f /etc/hosts ]; then echo "found"; fiBecause [ is a command, every rule you already know applies with full force. The spaces are mandatory — [-f is a Module-1 PATH search for a command named [-f (verdict: 127); the bracket must stand alone. Everything between [ and ] is arguments, so Module 2's expansion-then-splitting happens to them — which loads the gun for the trap in B3.
Where the analogy stops working. Hand a human inspector a form with a missing field and he asks about it. Hand [ a missing argument — which is precisely what an unquoted empty variable becomes after word splitting — and he does not see a blank field; he sees a different, shorter form and misjudges or errors on that (exit 2 and a message like unary operator expected). B3 springs this trap live.
🧪 Exercise 5.2 — meet the inspector
The last command fails on purpose.
type test
type [
[ -f /etc/hosts ] ; echo "$?"
[ -f /etc/nosuchfile ] ; echo "$?"
[-f /etc/hosts] ; echo "$?" # ← spaces removed: not brackets anymore✅ Expected result — click to reveal (contains a deliberate failure)
test is a shell builtin
[ is a shell builtin
0
1
bash: [-f: command not found
127What to read out of it: bash itself testifies that both names are builtins — commands, not syntax. The two bracketed runs produce clean verdicts: 0 (exists) and 1 (doesn't) — usable naked with &&/|| or dressed in if. The finale is Module 1 crashing the party: remove the space and bash searches PATH for a command literally named [-f — exit 127, command not found. If you ever see that error pointing at something bracket-shaped, you now diagnose it in one second: missing space, nothing deeper.
B2. The questions the inspector can check
The inspector's form catalogue, organized by what is being inspected. Files:
| Test | PASS (exit 0) when… | Typical production use |
|---|---|---|
| -e path | the path exists (any type) | "is there anything at this path?" |
| -f path | exists and is a regular file | config present before reading it |
| -d path | exists and is a directory | target dir present before writing into it |
| -r / -w / -x path | you may read / write / execute it | permission preflight (Module 3 Ticket 4's guard) |
| -s path | exists and is not empty | "did the backup actually contain bytes?" |
Strings — where -z asks "is it empty?" and -n "is it non-empty?", and = / != compare two of them. Numbers — a separate operator family: -eq -ne -gt -ge -lt -le. Two families because text and numbers disagree about the world: as strings, "10" < "9" is TRUE (character 1 sorts before 9, like dictionary order); as numbers, obviously false. Use = for strings, -eq for integers, and never trust a comparison until you know which family it is running in.
🧪 Exercise 5.3 — files, strings, numbers
cd ~/bash-course
[ -s users.txt ] && echo "users.txt has content"
[ -x hello.sh ] && echo "hello.sh is executable"
x=10
[ "$x" -gt 9 ] && echo "numerically: 10 > 9, of course"
[ "$x" \< "9" ] && echo "as STRINGS: 10 sorts before 9" # \< escapes < from Module 4's redirection meaning
name=""
[ -z "$name" ] && echo "name is empty"✅ Expected result — click to reveal
users.txt has content
hello.sh is executable
numerically: 10 > 9, of course
as STRINGS: 10 sorts before 9
name is emptyWhat to read out of it: the shock line is the fourth — the same value, compared in the string family, sorts the "wrong" way, PASSes, and prints. Nothing malfunctioned: dictionary order says 10 precedes 9 because 1 precedes 9. Version strings are where this bites production ("10.2.0" < "9.9.9" as strings!). Note also the \< — inside [ ], a bare < would be Module 4's input redirection, because [ is a command being handed arguments; the backslash (Module 2's third quoting tool) keeps it literal. [[ in Part C removes that particular awkwardness.
B3. The unquoted-variable trap, sprung on purpose
Module 2 promised the quoting reflex would keep paying off. Here is its finest hour. [ $name = "alice" ] with name empty: expansion replaces $name with nothing, word splitting removes the nothing, and the inspector receives the form [ = alice ] — three arguments where four were expected. He does not know a field is missing; he errors on the form he got.
🧪 Exercise 5.4 — the vanishing argument
The first test fails on purpose — with an error, not a verdict.
name=""
[ $name = "alice" ] ; echo "$?" # ← unquoted empty variable vanishes
[ "$name" = "alice" ] ; echo "$?" # quoted: empty string stays a (empty) argument✅ Expected result — click to reveal (contains a deliberate failure)
bash: [: =: unary operator expected
2
1What to read out of it: the unquoted version did not return "false" — it returned 2 with a syntax complaint, the inspector's "this form is malformed." (unary operator expected = "I got = alice ] and thought = must be a one-argument test, which it isn't.") The quoted version returns a clean, meaningful 1: checked, not equal. The difference matters enormously in if: both non-zero verdicts skip the then-block, so the buggy script often appears to work — while spraying errors to stderr and conflating "malformed check" with "checked: no." Quote every expansion inside [ ], no exceptions — this is Module 2's reflex with a badge.
Interview questions — Part B
🎯 "What is the difference between == and -eq in shell scripting?" — asked verbatim at Hirist
The direct answer: == (and POSIX =) compares strings; -eq compares integers. [ "10" == "10" ] and [ 10 -eq 10 ] both pass, but [ "010" == "10" ] fails (different text) while [ 010 -eq 10 ] passes (same number).
Going deeper: give the ordering hazard, not just equality — string < is dictionary order, so "10" < "9" is true, which is how version comparisons go wrong; and note -eq on a non-number is an error (exit 2, integer expression expected), not false. Mention that inside [ ], = is the portable spelling and == a bash-ism; inside [[ ]], == additionally does pattern matching against an unquoted right side.
The details that separate candidates: the exit-2-vs-exit-1 distinction (malformed test vs false test) and its debugging signature; plus the honest answer to "how do I compare versions properly?" — not with either family; use sort -V (Module 11) or dedicated tooling.
🎯 "How to check if a directory exists?" — asked verbatim at Edureka; Zero To Mastery asks "How do you check for file or directory existence in Bash?"
The direct answer: if [ -d "$dir" ]; then … — with -f for regular files, -e for "anything at this path."
Going deeper: quote the variable (Edureka's own published answer writes [ -d $mydir ] unquoted — you can now explain precisely why that is a bug waiting for an empty or spacey value); pick the right operator deliberately (-e passes for a directory where you needed a file — a real class of bug); and add the permission dimension: existence is not access; -r/-w/-x check what you may actually do.
The details that separate candidates: the negative forms in guard style — [ -d "$dir" ] || { echo "missing: $dir" >&2; exit 1; } — showing conditionals fused with Module 3's guard-clause idiom and Module 4's stderr discipline in one production-shaped line.
Part C — [[ … ]]: the inspector who works for bash directly
C1. What changes inside double brackets
Bash offers a second inspector: [[ … ]]. Unlike [, this one is not a command — it is real syntax, parsed by bash before the usual rewriting machinery runs. That single design change buys four practical upgrades:
1. The vanishing-argument trap is disarmed. No word splitting, no globbing happen between [[ and ]] — [[ -n $file ]] is safe even unquoted, even with spaces in the value. (Keep quoting anyway; the reflex must survive contact with [-style code, and quoted is never wrong.)
2. < and > compare strings without backslashes. They cannot mean redirection here, because bash knows it is inside a test.
3. == pattern-matches. With an unquoted right-hand side, [[ "$host" == web-* ]] asks "does the value fit this shape?" — Module 2's glob language, repurposed for strings. Quote the right side and it degrades to plain equality.
4. =~ matches regular expressions. [[ "$input" =~ ^[0-9]+$ ]] — "entirely digits" — is the standard numeric-input validator. (Regex is Module 11's language; until then, treat this one pattern as a recipe.)
The cost: [[ ]] is bash-only. In #!/bin/sh scripts (Module 1 C2's territory) it does not exist — dash greets it with [[: not found. The professional rule: [[ ]] in bash scripts, [ ] when writing portable sh — and never a mix out of habit.
Where the analogy stops working. In-house convenience creates a monoculture: the insider exists only in this building (bash). Send your script to a building staffed differently (/bin/sh on Ubuntu = dash) and every [[ line dies with not found. The contractor, for all his mailroom risk, works everywhere — which is exactly the portability trade from Module 1 C2, recurring at the level of a single bracket.
🧪 Exercise 5.5 — the upgrades, live
file="monthly report.txt"
[[ -n $file ]] && echo "unquoted, yet safe (in [[ only!)"
[[ "10" < "9" ]] && echo "string order still says 10 < 9"
host="web-42"
[[ "$host" == web-* ]] && echo "pattern: a web host"
input="123"
[[ "$input" =~ ^[0-9]+$ ]] && echo "input is numeric"✅ Expected result — click to reveal
unquoted, yet safe (in [[ only!)
string order still says 10 < 9
pattern: a web host
input is numericWhat to read out of it: line 1 would have been the B3 explosion under [ ] — here it just works: the space in the value never became an argument boundary. Line 2 is a warning shot: [[ fixes splitting, not semantics — < is still dictionary order, so the 10-before-9 surprise survives. Lines 3–4 are the genuinely new powers: shape-matching with globs and regex. One habit to carry out of this exercise: pattern right sides stay unquoted on purpose (web-*), value right sides get quotes — in [[, quoting the right side is how you say "literal, not pattern."
Interview questions — Part C
🎯 "What is the difference between [ and [[ in bash?" — the classic screener; rarely published with exact wording, but it is the substance behind Zero To Mastery's and KnowledgeHut's conditional questions, and whole reference articles exist on it
The direct answer: [ is a command (alias of test, POSIX-portable) whose arguments undergo normal expansion — quoting is critical. [[ is bash syntax: no word splitting or globbing inside, </> compare strings directly, == pattern-matches an unquoted right side, =~ matches regex — but it does not exist in plain sh.
Going deeper: demonstrate the divergence with one empty variable — [ $x = y ] errors (unary operator expected, exit 2) where [[ $x = y ]] calmly returns false — and explain why: arguments-after-expansion versus parsed-before-expansion.
The details that separate candidates: the decision rule stated as policy (bash script → [[; #!/bin/sh → [ with religious quoting); knowing &&/|| work inside [[ ]] but not inside [ ] (there you need -a/-o, which are deprecated and ambiguous); and the regex caveat that the =~ pattern should be unquoted or stored in a variable — quoting it makes it literal.
🎯 "How do you validate numeric input in shell scripts?" — asked verbatim at Hirist
The direct answer: [[ "$input" =~ ^[0-9]+$ ]] — anchored regex, "one or more digits and nothing else." Non-numeric (including empty) fails the match, and you reject with a usage message on stderr and exit 2 (Module 3's convention).
Going deeper: explain why the naive alternative is wrong — [ "$input" -eq "$input" ] "works" by exploiting the error behavior of -eq on non-numbers (exit 2 plus stderr noise), which conflates malformed-with-false and pollutes logs. The regex states intent; anchors ^/$ are the part candidates forget (unanchored [0-9] happily accepts abc123).
The details that separate candidates: edge-case awareness — negative numbers (^-?[0-9]+$), leading zeros (later arithmetic may parse them as octal), and empty-string behavior; plus where validation belongs: at the top of the script, on arguments, before any work (the fail-fast shape Module 7 completes).
Part D — case: one value, many shapes
D1. The switchboard
When one value must route to one of many branches — an environment name, a subcommand, a $rc from Module 3 — an elif-ladder works but reads like bureaucracy. case is the purpose-built switchboard:
case "$env" in
prod|production) echo "deploying carefully" ;;
staging) echo "deploying to staging" ;;
dev*) echo "anything goes" ;;
*) echo "unknown environment: $env" >&2 ; exit 2 ;;
esacThe left sides are patterns (the same glob language as [[ ... == ... ]] — literals, *, ?, and | for alternatives), tried top to bottom; the first match wins and only its branch runs. ;; closes a branch; esac (case backwards) closes the construct; and the final *) is the switchboard's "all other callers" jack — in production it is mandatory in spirit: an unmatched value falling silently through a case is a bug you find weeks later.
Where the analogy stops working. A human sorter notices when two hole-labels overlap and asks which wins. case never notices: order is the only tiebreak — put dev* above devops-prod and the specific label never gets mail. The discipline: specific patterns first, families after, * last, and re-check ordering every time you add a branch.
🧪 Exercise 5.6 — route by shape
cd ~/bash-course
nano route.sh # the case block below, wrapped in a script:#!/bin/bash
env="$1"
case "$env" in
prod|production) echo "deploying carefully" ;;
staging) echo "deploying to staging" ;;
dev*) echo "anything goes" ;;
*) echo "unknown environment: $env" >&2 ; exit 2 ;;
esacchmod +x route.sh
./route.sh staging
./route.sh production
./route.sh dev-local
./route.sh qa ; echo "verdict: $?"(One peek ahead: $1 means "the first word typed after the script's name" — Module 7 owns that syntax; today it is just the wire that gets your test values into the case.)
✅ Expected result — click to reveal (the last run fails on purpose)
deploying to staging
deploying carefully
anything goes
unknown environment: qa
verdict: 2What to read out of it: four callers, four routes. production rode the | alternative; dev-local matched the dev* family; and qa — the value nobody planned for — hit the catch-all, which did everything right at once: named the bad value, spoke on stderr, and exited 2 (usage-error convention, Module 3). That one catch-all line is the difference between "the deploy tool rejected qa" and "the deploy tool did nothing and nobody knows why."
D2. Choosing your conditional: the decision tree
Diagram source
flowchart TD
A["I need to branch"] --> B{"On what?"}
B -->|"a command's<br>success/failure"| C["if command; then<br>(no brackets at all)"]
B -->|"a question about<br>files/strings/numbers"| D{"script dialect?"}
B -->|"one value vs<br>many patterns"| E["case ... esac<br>specific first, * last"]
D -->|"#!/bin/bash"| F["if [[ ... ]]<br>splitting-safe, patterns, =~"]
D -->|"#!/bin/sh<br>portable"| G["if [ ... ]<br>quote every expansion"]
C --> H{"just a guard<br>one-liner?"}
H -->|"yes"| I["command || exit 1<br>(Module 3 style)"]
H -->|"real else-branch"| J["full if/else/fi"]Interview questions — Part D
🎯 "Explain the use of case statement in shell scripting" — asked at KnowledgeHut ("How do you use conditional statements (e.g., if, case)…") and listed at Hirist
The direct answer: case "$value" in pattern) commands ;; … esac routes one value through glob patterns, first match wins — the readable replacement for elif-ladders keyed on a single variable; standard for subcommand dispatch (start|stop|restart) and environment routing.
Going deeper: the left sides are patterns, not strings — | alternatives, * families — and order is the only precedence, so specific-before-general is a correctness rule, not style. A defensive *) branch that names the unmatched value on stderr and exits non-zero belongs in every production case.
The details that separate candidates: knowing the ;& and ;;& fallthrough variants exist in bash (;& falls through, ;;& keeps testing) but are rare and version-bound — mentioning them and advising against them reads as experience; and the pairing insight: case for routing, [[ for questions — the constructs complement rather than compete.
🎯 "Determine the output of the following command: [ -z \"\" ] && echo 0 || echo 1" — asked verbatim at Edureka
The direct answer: 0. -z "" asks "is the empty string empty?" — yes, exit 0 — so && fires and echoes 0; the || is skipped because echo succeeded.
Going deeper: the question is a two-module trap check — you must know both -z (this module) and the a && b || c mechanics (Module 3): had the echo somehow failed, 1 would print as well. Walking the verdicts aloud, operator by operator, is the answer format interviewers want.
The details that separate candidates: pointing out the cruel joke in the question's design — it prints 0 for true and 1 for false, deliberately inverting every other language's convention while exactly matching bash's exit-code convention (Module 3's counter-intuitive zero). Candidates who name that inversion, unprompted, demonstrate they hold both mental models at once.
Part E — Production
E1. 🏭 Production practices
Guards first, work second. Scripts open with a block of preflight conditionals — required files (-f), directories (-d), permissions (-r/-w/-x), non-empty inputs (-s) — each failing fast with a named error on stderr and a meaningful exit code, before any real work begins.
[[ ]] in bash scripts; [ ] only in deliberate #!/bin/sh scripts — and quoted to the teeth there. Mixing the two styles in one file is treated as a review flag: it usually means copy-paste from mismatched sources.
Every expansion inside [ ] is quoted, and ShellCheck enforces it. SC2086 and friends turn the vanishing-argument trap into a CI failure instead of a 3 a.m. page.
Every case has a *) branch that names the value, speaks on stderr, and exits non-zero. Silent fallthrough is a defect class of its own.
String and number comparisons are never mixed. =/== for text, -eq family for integers; version strings go to sort -V or real tooling, never to <.
Complex conditions get a name. When a guard grows past two clauses, production code wraps it in a function with a speaking name (is_prod_host, Module 8) — the if-line then reads as policy, not puzzle.
E2. Production-practice table
| Symptom | What is really happening | What to run | The fix |
|---|---|---|---|
| [: =: unary operator expected in the logs; the if "sort of works anyway" | An unquoted variable expanded to nothing and vanished from the test's arguments — exit 2 masquerading as false | printf '[%s]\n' "$var" (Module 2's probe) on the suspect variable | Quote every expansion inside [ ]; adopt [[ ]] in bash scripts; let ShellCheck catch strays |
| [-f: command not found (exit 127) | Missing space — [ is a command and needs to stand alone as a word | Look at the failing line; the error names the mashed-together word | [ -f file ] with spaces around both brackets |
| A version check waves through 10.x as "older than 9.x" | String comparison — dictionary order sorts "10…" before "9…" | [ "10.2" \< "9.9" ] && echo bug — watch it pass | Compare numbers with -lt per component, or sort -V (Module 11); never raw string order for versions |
| [[: not found on some hosts/containers only | The script says #!/bin/sh (or runs under sh) — dash has no [[ (Module 1 C2's dialect trap, bracket edition) | head -1 script.sh and ls -l /bin/sh on the failing host | Shebang #!/bin/bash if bash features are wanted, or rewrite tests portably with [ ] |
| A case branch never runs, no matter the input | An earlier, broader pattern (dev* above dev-prod) is swallowing its mail — order is the only precedence | Read the patterns top to bottom against the failing value, first-match rules | Reorder specific-before-general; add a test run per branch after any edit |
| Unknown value sails through a case silently; downstream breaks later | No *) catch-all — the switchboard dropped the call on the floor | Feed the case a nonsense value and watch nothing happen | Add *) echo "unexpected: $value" >&2 ; exit 2 ;; to every production case |
E3. 🎓 Capstone — four tickets from the queue
🎓 Ticket 1 — "Our disk-space monitor pages when usage exceeds the threshold. Last night it paged claiming 9% > 85%. The check: if [ \"$usage\" \\> \"$threshold\" ]."
Diagnosis. String comparison doing string things (B2): with usage=9 and threshold=85, dictionary order compares first characters — 9 sorts after 8 — so "9" > "85" is TRUE. The monitor has also been silent about real overruns like 100 vs 85 ("100" < "85" as strings), which is the scarier half of the ticket.
Work the steps: reproduce both directions: [ "9" \> "85" ] && echo false-page and [ "100" \> "85" ] || echo missed-alert. Two one-liners, whole bug demonstrated.
Fix and prevention: numeric family: if [ "$usage" -gt "$threshold" ] — plus input validation upstream ([[ "$usage" =~ ^[0-9]+$ ]]) so a df hiccup that yields 9% or an empty string is rejected loudly (exit 2, message on stderr) instead of compared creatively. Review rule: any \> or \< inside [ ] near numbers is presumed a bug until proven a deliberate string sort.
🎓 Ticket 2 — "Deploy script: if [ $CONFIRM = \"yes\" ]; then deploy; fi. Works for everyone except CI, where it spams unary operator expected — but the deploy is skipped, so 'no harm done,' says the author."
Diagnosis. B3's vanishing argument: humans always export CONFIRM; CI doesn't, the unquoted empty expansion evaporates, [ receives [ = yes ], errors with exit 2 — which happens to skip the then-block, so the bug impersonates correct behavior while logging garbage. "No harm done" is luck, not design: flip the logic to != for a safety check and the same malformation would run the guarded action.
Work the steps: CONFIRM= bash -c '[ $CONFIRM = yes ]; echo $?' → error + 2; quoted variant → clean 1.
Fix and prevention: quote it — [ "$CONFIRM" = "yes" ] — or [[ $CONFIRM == yes ]] in this bash script; then make the malformed state impossible: : "${CONFIRM:?CONFIRM must be set}"-style required-variable checks (Module 9 owns that syntax) or set -u. Exit-2-with-stderr is never "fine" in a deploy path, even when today's routing is lucky.
🎓 Ticket 3 — "Service dispatcher: case $1 in start*) …;; stop) …;; status) …;; esac. Someone ran ./svc.sh startover and it started the service. Separately, ./svc.sh restart does nothing at all — no output, exit 0."
Diagnosis. Two switchboard sins in one case (D1). Sin one: the sloppy family pattern start* accepts startover, started, startanything — pattern generosity nobody intended. Sin two: no *) catch-all, so restart — never wired — fell on the floor silently with the last branch's happy exit code.
Work the steps: feed the case its own patterns: startover (matches branch 1 — why?), restart (matches nothing — then what runs? nothing). Both answers are visible from reading top to bottom with first-match rules.
Fix and prevention: exact labels for commands — start) … ;; stop) … ;; restart) … ;; status) … ;; — families like start* are for genuinely family-shaped data (hostnames, versions), not verbs; and the non-negotiable *) echo "usage: $0 {start|stop|restart|status}" >&2 ; exit 2 ;;. A dispatcher that cannot say "I don't know that command" is not dispatching; it is gambling.
🎓 Ticket 4 — "A hardening script must run only on staging: if [[ $HOSTNAME == *staging* ]]. It just ran on prod-staging-gateway — which is a production host — and hardened it into an outage."
Diagnosis. Pattern generosity again, at the [[ level (C1): *staging* matches any hostname containing staging, and someone named a prod box prod-staging-gateway. The conditional did exactly what the pattern said; the pattern said more than the author meant. (Naming is data — and data drifts; Module 2's fleet warning in a new costume.)
Work the steps: enumerate what the pattern accepts: for h in staging-1 prod-staging-gateway; do [[ $h == *staging* ]] && echo "$h matches"; done — both print (loop syntax is Module 6, arriving next; read it as "try each value").
Fix and prevention: anchor the intent — [[ $HOSTNAME == staging-* ]] if staging hosts follow staging-N, or better, stop deriving authorization from names: check an explicit marker ([ -f /etc/deploy-tier ] && [[ $(cat /etc/deploy-tier) == staging ]]) that provisioning writes deliberately. Naming conventions are documentation; markers are contracts. Destructive scripts get contracts.
E4. Documentation reference
| Topic | Authoritative reference | What you'll find there |
|---|---|---|
| if, case, [[ ]] | Bash manual — Conditional Constructs | Exact syntax and semantics of all three constructs, including ;;/;&/;;& |
| Every test operator | Bash manual — Bash Conditional Expressions | The complete catalogue: file, string, and arithmetic tests with precise definitions |
| test / [ as commands | test(1) — man7.org · Bash manual — Bourne Shell Builtins | The standalone command's rules — and confirmation that [ is just its other name |
| The patterns case and == use | Bash manual — Filename Expansion | The glob language (*, ?, […]) shared by Module 2's globbing, case, and [[ == ]] |
E5. Self-assessment
Answer from memory, out loud or on paper. Every answer is in this module.
- What does if actually evaluate? Why does if ./deploy.sh; then work with no brackets anywhere?
- What is [, literally? Name three consequences that follow from that fact.
- Why are the spaces in [ -f file ] mandatory, and what error appears without them?
- -e vs -f vs -d vs -s — one clause each, and one production use each.
- With name="", why does [ $name = "alice" ] exit 2 rather than 1 — and why is that distinction operationally important?
- = vs -eq: which family for which data, and what does [ "10" \< "9" ] teach about the difference?
- Name the four things [[ ]] does that [ ] cannot, and the one thing [ ] does that [[ ]] cannot.
- In [[ $host == web-* ]], why must web-* stay unquoted? What changes if you quote it?
- Write the standard numeric-input validator and explain both anchors.
- In a case statement, what determines which branch runs when several patterns could match? What ordering discipline follows?
- Why does every production case end with *), and what three things should that branch do?
- Walk through [ -z "" ] && echo 0 || echo 1 verdict by verdict. What prints, and why is the output's numbering a joke at your expense?
E6. Sources
Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "How do you use conditional statements in Bash?", "How do you check for file or directory existence in Bash?" (published June 18, 2026)
Hirist — Top 30+ Shell Scripting Interview Questions and Answers — "What is the difference between == and -eq in shell scripting?", "How do you validate numeric input in shell scripts?" (published Jul 22, 2025; last modified Dec 31, 2025)
Edureka — Top 60 Shell Scripting Interview Questions and Answers — "How to check if a directory exists?", "Determine the output of the following command: [ -z "" ] && echo 0 || echo 1" (page updated Dec 9, 2024)
KnowledgeHut — Shell Scripting Interview Questions and Answers — "How do you use conditional statements (e.g., if, case) in a shell script?" (no publication date shown on page)
A note on the corpus: the [ vs [[ distinction — arguably the most interview-tested fact in this module — circulates in countless real screens and reference articles but rarely appears in published lists with a fixed wording; its summary line above says so rather than inventing a citation. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2.