Module 3 — Exit Codes and Command Chaining
Updated 3 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
Part A — The hidden verdict
A1. Every command ends with a number
When any command finishes — success or disaster — it hands the shell one small number on its way out: the exit status (also called exit code or return code). The convention is absolute and slightly backwards-feeling: 0 means success. Any other number (1–255) means failure. Text output is for humans; the exit status is the machine-readable verdict, and it exists even for commands that print nothing at all.
Bash stores the most recent verdict in a special variable: $?. Read it like any variable — echo "$?" — and you are reading the result of the command just before.
Where the analogy stops working. The depot board keeps history. Bash keeps only the latest code — $? is overwritten by every command that runs, including the very command you use to look at it. Part D turns that quirk into a habit.
🧪 Exercise 3.1 — see the verdict
The second command fails on purpose.
date
echo "$?" # verdict on date
ls /nonexistent # ← fails
echo "$?" # verdict on that failure✅ Expected result — click to reveal (contains a deliberate failure)
Thu Sep 3 12:39:38 +08 2026
0
ls: cannot access '/nonexistent': No such file or directory
2What to read out of it (your date and timezone will differ): date succeeded → 0. The failed ls printed a human-readable complaint on the screen and left the machine-readable 2 in $? — two channels, one event. Why 2 and not 1? Each program chooses its own failure numbers and documents them; GNU ls uses 2 for "serious trouble" (its man page says so). The habit being built: after any suspicious command, ask echo "$?" immediately — the next section explains the urgency.
A2. The failure vocabulary: which numbers mean what
There is no universal table of what 1 or 2 means — each program defines its own codes and documents them in its man page (grep is a beautiful example: 0 = found a match, 1 = no match, 2 = real error — three verdicts, three different follow-up actions). But a few numbers are reserved by bash itself, and you have already caused two of them:
| Code | Meaning | You met it in |
|---|---|---|
| 0 | Success — the only success | every working example so far |
| 1, 2, … | Program-defined failures — read the program's man page | ls uses 2 for "cannot access" |
| 126 | Found the file, but it is not executable | Module 1's Permission denied |
| 127 | Command not found at all — the PATH search failed | Module 1's command not found |
| 128+N | Killed by signal number N (e.g. 130 = Ctrl-C) | coming in Module 13 |
The distinction between 126 and 127 is Module 1's Part D decision tree, now in numeric form — monitoring systems that only see exit codes can still tell "script missing" from "script not executable" by these two numbers alone.
🧪 Exercise 3.2 — collect the reserved codes deliberately
Both commands fail on purpose.
nonexistentcmd # ← nothing by this name anywhere on PATH
echo "$?"
bash -c 'exit 3' # a throwaway bash that quits with a code WE chose
echo "$?"✅ Expected result — click to reveal (contains deliberate failures)
bash: nonexistentcmd: command not found
127
3What to read out of it: the PATH-search failure is exactly code 127 — message and number are two views of one event. The second experiment is the important one: exit 3 shows that an exit code is just a number the exiting program picks. Any script you write can pick its own numbers to describe its own failures — and in Part C you will. (The bash -c wrapper is Module 2's briefing-pack child: it quits with code 3, and our shell reads the verdict from outside.)
Interview questions — Part A
🎯 "What does an exit code of 0 signify, and what do non-zero codes indicate?" — a staple opener. LinuxTeck's exit-code guide covers it as core material, but no widely-published verbatim wording exists — treat the topic, not the phrasing, as the question
The direct answer: 0 always means success; any non-zero value (1–255) signals failure, with the specific number chosen by the program to describe which failure. The most recent code is readable in $?.
Going deeper: name the reserved values — 126 found-but-not-executable, 127 not found, 128+N killed by signal N — and note that ordinary failure codes are program-specific conventions documented per man page (grep's 0/1/2 being the canonical example of codes carrying meaning, not just failure).
The details that separate candidates: explaining why zero is success (one way to succeed, many ways to fail — the failure space needs the numbers); knowing codes cap at 255 (the value is one byte — exit 300 wraps around); and connecting codes to automation: CI steps, monitoring, and && chains act on the number, never on the printed text.
🎯 "How do I check the exit status of the last command in Linux?" — asked verbatim in LinuxTeck's FAQ; Edureka phrases it "How to check if the previous command was run successfully?"
The direct answer: read $? immediately after the command — echo "$?" to view it, or rc=$? to keep it.
Going deeper: the word immediately is the substance — every command overwrites $?, including the echo you used to inspect it, so production scripts capture it into a named variable on the very next line and reason about the copy. (Part D of this module drills the habit.)
The details that separate candidates: mentioning that in real scripts you usually never touch $? at all — you feed the verdict directly to &&, ||, or if command (Module 5), which read it invisibly; explicit $? capture is for when you need the specific number, e.g. distinguishing grep's "no match" (1) from grep's "error" (2). That distinction — implicit use versus explicit capture — marks people who have written real automation.
Part B — Chaining: letting verdicts drive the next command
B1. Three glues: ;, &&, ||
You can put several commands on one line, and the glue between them decides how verdicts flow:
| Glue | Reads the verdict? | Meaning |
|---|---|---|
| a ; b | no | run a, then run b regardless — exactly like two lines of a script |
| a && b | yes | run b only if a succeeded (exit 0) — "and then" |
| a || b | yes | run b only if a failed (non-zero) — "or else" |
This is the answer to the discomfort planted in Modules 1 and 2: bash carries on past failures by default — because a script's newlines are ; glue. Choosing && is how you opt into "stop the parade if this step fails." Two tiny commands exist purely for experimenting with this machinery: true (does nothing, exits 0) and false (does nothing, exits 1) — a success pill and a failure pill.
Where the analogy stops working. A human intern eventually notices the fire. ; never does — a 40-line script glued by newlines will cheerfully run line 40 against a database that line 2 failed to create. That is not a bash flaw to forgive but a default to manage: && locally (this module) and set -e globally (Module 14) are the management tools.
🧪 Exercise 3.3 — the three glues, felt
Several commands here fail on purpose.
ls /nonexistent ; echo "semicolon: I run anyway"
ls /nonexistent && echo "and: you will not see this"
ls /nonexistent || echo "or-else: fallback ran"
mkdir -p /tmp/rel && echo "and: directory ready"✅ Expected result — click to reveal (contains deliberate failures)
ls: cannot access '/nonexistent': No such file or directory
semicolon: I run anyway
ls: cannot access '/nonexistent': No such file or directory
ls: cannot access '/nonexistent': No such file or directory
or-else: fallback ran
and: directory readyWhat to read out of it, glue by glue: after ; the echo ran despite the failure. After && — look carefully — there is no echo line at all: the failure vetoed it silently (the second cannot access line belongs to the next command's ls). After || the fallback fired. Last line: success feeding && lets the chain proceed. One more verdict subtlety worth knowing: the exit status of a whole a && b line where a fails is a's code (here 2) — the chain reports the first failure, which is exactly what a CI system wants to see.
B2. The a && b || c trap
Chains longer than two read left to right, and a famous "obvious" pattern hides a bug. People write deploy && echo ok || alert meaning "if deploy works say ok, otherwise alert" — if/else in one line. Almost. The || does not watch deploy; it watches whatever ran just before it. If deploy succeeds but the echo itself fails, the alert fires too. With echo the risk is tiny (echo can fail, e.g. writing to a closed output — Module 4 makes that scenario concrete), but replace it with anything less trivial and the pattern misfires for real.
🧪 Exercise 3.4 — prove the trap with pills
This one-liner misfires on purpose.
true && false || echo "cleanup ran (surprise!)"✅ Expected result — click to reveal (contains a deliberate misfire)
cleanup ran (surprise!)What to read out of it: read it as the pattern-user intended — "true succeeded, so the else-branch must not run" — and yet it ran. Trace the verdicts left to right: true → 0, so && runs false; false → 1; || sees that 1 and fires. The || never knew true existed. If this exercise's one line of output ever shows up in your own script's logs when the main command succeeded, you now know exactly which pattern to go hunting for.
Interview questions — Part B
🎯 "What is the difference between && and || in Bash?" — asked verbatim at Zero To Mastery
The direct answer: both read the previous command's exit status. a && b runs b only on success (0); a || b runs b only on failure (non-zero). They short-circuit — the skipped command never starts at all.
Going deeper: give the canonical uses — mkdir -p dir && cd dir (do not enter a directory that failed to appear), command || echo "failed" (self-reporting steps) — and the composed-pattern caveat: a && b || c is not if/else, since || reacts to b's failure too. Bind real else-logic to if.
The details that separate candidates: stating what the whole chain's exit status is (the last command that actually ran — so a && b with a failing propagates a's code); and knowing the precedence is flat left-to-right for &&/|| (equal precedence, no "and binds tighter" — another difference from most programming languages).
🎯 "What is the purpose of the exit command in a bash script?" — a perennial, asked in countless variations with no canonical published wording to cite
The direct answer: exit N ends the script immediately and hands N to whoever launched it as the script's own exit status. Plain exit uses the status of the last command run.
Going deeper: without any exit, a script's status is simply the last command's status — a script whose final line happens to fail reports failure, and one whose final line succeeds reports success even if earlier lines failed (the ;-glue default). Explicit exit 0 / exit 1 at meaningful points is how a script becomes a trustworthy building block for && chains, CI steps, and monitoring.
The details that separate candidates: designing a small code vocabulary for your own scripts (0 ok, 1 general failure, 2 bad usage — mirroring bash's own convention, as LinuxTeck's companion question about invalid arguments suggests) and documenting it in the script header; plus one subtlety from Module 1's world: exit in a sourced file terminates the calling shell itself — scripts meant for sourcing must return instead (Module 8).
Part C — Verdicts you can build on
C1. Commands as questions: grep -q
Chaining becomes powerful the moment you meet commands designed to answer questions with their exit code. The first one every DevOps engineer learns: grep pattern file searches a file for lines containing a pattern (a full treatment of grep's pattern language arrives in Module 11 — plain words work fine until then). Add -q (quiet) and grep prints nothing, communicating entirely by verdict: 0 = found, 1 = not found, 2 = could not even search (file missing, unreadable). Suddenly && and || can act on facts about your systems: is this host in the config? does the log mention the error? is the setting present?
Where the analogy stops working. A clerk on the phone might add "…but the archive room was locked, so I'm not sure." grep's verdict channel carries exactly one number: with -q, "no" (1) and "couldn't look" (2) both arrive with no match output — though on 2 grep still prints its complaint to the error channel (silencing that too takes -s, Module 4 territory) — and they stay indistinguishable unless you check which number: rc=$? and compare. Automation that treats "couldn't look" as "not there" has caused real incidents: an unreadable config file is not the same as a feature being disabled.
🧪 Exercise 3.5 — asking questions with verdicts
One command fails on purpose (that is the "no" answer).
cd ~/bash-course
printf 'alice\nbob\ncarol\n' > users.txt # printf writes three lines into users.txt (\n = end of line, Module 1's printf)
grep -q bob users.txt && echo "bob present"
grep -q zoe users.txt || echo "zoe missing"
grep -q zoe users.txt ; echo "$?"✅ Expected result — click to reveal (contains a deliberate failure)
bob present
zoe missing
1What to read out of it: three questions, three verdict-driven outcomes, and not one line of grep's own output anywhere — -q kept the whole conversation on the verdict channel. The final 1 is grep's documented "searched fine, found nothing." Had users.txt been missing, that number would have been 2, plus a grep complaint on the error channel — different number, different channel, exactly the distinction the analogy warned about. (Also notice printf writing a file with > — a preview stolen from Module 4, used here only to give grep something to search.)
C2. Giving your own script a verdict
Your scripts are commands too — Module 1 made them runnable; this section makes them report. Two rules govern what ./yourscript.sh leaves in the caller's $?:
Rule 1 — no explicit exit: the script's status is whatever its last command's status was. Convenient, and dangerous: add a harmless final echo "done" to a failing script and you have painted the failure green.
Rule 2 — explicit exit N: the script stops right there and reports N. This is how you build guard clauses: grep -q "localhost" /etc/hosts || exit 1 reads as "if the sanity check fails, stop now and say so."
🧪 Exercise 3.6 — two scripts, two verdicts
The second script reports failure on purpose.
cd ~/bash-course
nano check-hosts.sh # 3 lines:#!/bin/bash
grep -q "localhost" /etc/hosts || exit 1
echo "hosts file looks sane"nano lastword.sh # 3 lines — no explicit exit anywhere:#!/bin/bash
echo "doing work"
ls /nonexistentchmod +x check-hosts.sh lastword.sh
./check-hosts.sh ; echo "verdict: $?"
./lastword.sh ; echo "verdict: $?"✅ Expected result — click to reveal (contains a deliberate failure)
hosts file looks sane
verdict: 0
doing work
ls: cannot access '/nonexistent': No such file or directory
verdict: 2What to read out of it: check-hosts passed its guard (every Linux hosts file names localhost — if yours somehow does not, the script exits 1 and prints nothing, which is itself the design working). lastword.sh demonstrates Rule 1: nobody wrote exit 2, but the failing ls was the last word, so its 2 became the whole script's verdict. Now mentally swap lastword's two lines: the script would end with the echo succeeding — verdict 0, failure hidden. Scripts that end in cleanup-ish commands report the cleanup, not the work; Module 14 closes that hole for good.
Part D — Handling verdicts like production code
D1. $? is perishable — copy it or use it instantly
The status board keeps only the latest code (Part A's analogy), and everything updates it — including your inspection tools. Run echo "$?" twice after a failure: the first prints the failure code; the second prints 0, the verdict on the first echo. Debug sessions go sideways exactly here — the evidence evaporates as you examine it. The production habit is a one-line reflex: rc=$? on the very next line, then reason about "$rc" at leisure (readonly it, log it, compare it — Module 2's tools all apply).
🧪 Exercise 3.7 — watch the evidence evaporate, then bag it
The first command fails on purpose; the point is what happens two lines later.
ls /nonexistent
echo "$?" # the verdict on ls
echo "$?" # ← the verdict on the previous echo!
ls /nonexistent
rc=$? # bag the evidence immediately
echo "saved: $rc, and it keeps: $rc"✅ Expected result — click to reveal (contains a deliberate failure)
ls: cannot access '/nonexistent': No such file or directory
2
0
ls: cannot access '/nonexistent': No such file or directory
saved: 2, and it keeps: 2What to read out of it: the middle 0 is the whole lesson — same command, one line later, different answer, because the successful first echo overwrote the board. After rc=$?, the copy is stable for the rest of the script. (And yes, the bagging is itself a command: after rc=$? runs, $? becomes 0 — the verdict on the successful assignment. The copy in rc is the only survivor, which is the whole point: bag evidence first, always.)
D2. The verdict-driven deploy, drawn
Diagram source
flowchart TD
A["./preflight.sh"] --> B{"exit 0?"}
B -->|"yes"| C["./deploy.sh"]
B -->|"no (1, 2, ...)"| X["stop — report<br>preflight's code"]
C --> D{"exit 0?"}
D -->|"yes"| E["./smoke-test.sh"]
D -->|"no"| Y["rollback || page-oncall<br>(rollback's own verdict<br>decides the page)"]
E --> F{"exit 0?"}
F -->|"yes"| G["announce success<br>exit 0"]
F -->|"no"| YThis is every CI pipeline you will ever configure, reduced to its skeleton: named steps, each a script with a trustworthy verdict (C2), glued by success (&& thinking), with || reserved for the recovery branch. When a pipeline product shows a red ✗ on a step, this diagram is what it is drawing.
Interview questions — Parts C–D
🎯 "What does exit code 127 mean in bash?" — asked verbatim in LinuxTeck's FAQ (its companion, also verbatim: "What does exit code 126 mean and how do I fix it?")
The direct answer: 127 = command not found — bash searched PATH and found nothing by that name (typo, missing package, wrong PATH). 126 = found but not executable — permissions, or a directory by that name. Resolve 127 by checking spelling and type -a name; resolve 126 with ls -l and chmod +x.
Going deeper: these are bash's codes, not the program's — the program never ran. That matters in monitoring: a job that flips from failing-with-1 to failing-with-127 did not "get worse," it stopped existing (deployment problem, not logic problem). Cron jobs failing with 127 are almost always PATH differences (cron's PATH is minimal — Module 15).
The details that separate candidates: pairing each number with its Module-1 message (command not found / Permission denied) and its fix command without hesitation; and knowing 128+N as the third reserved family so nothing in the 126–165 range ever looks mysterious again.
🎯 "What exit code should a script use for invalid arguments?" — a common follow-up in exit-code discussions, phrased many ways with none canonical; the convention below is the substance interviewers probe for
The direct answer: exit with code 2 for usage errors — wrong or missing arguments — mirroring bash's own convention (its builtins return 2 for incorrect usage), and print a usage message before exiting (to the error channel, Module 4).
Going deeper: the wider convention worth stating: 0 success, 1 general failure, 2 usage error, keep 126+ clear of the reserved meanings. What matters most is consistency within your fleet's scripts — the numbers are an API for the callers (CI, cron, other scripts), and callers write case "$rc" logic against them (Module 5).
The details that separate candidates: argument validation itself belongs at the top of the script as guard clauses ([ $# -eq 2 ] || { usage; exit 2; } — the pieces arrive in Modules 5 and 7), so the script fails in microseconds on bad input instead of failing halfway through real work — the property operators actually care about.
Part E — Production
E1. 🏭 Production practices
Every script is written to leave a truthful verdict. Guard clauses command || exit 1 at the top, explicit exit 0 where success is decided, and never a decorative final echo that repaints a failure green. Scripts are building blocks; their exit codes are the API.
A small, documented exit-code vocabulary. 0 success, 1 general failure, 2 usage error, and any richer codes listed in the script's header comment — mirroring bash's own conventions and staying clear of the reserved 126/127/128+N.
&& for dependent steps, || for recovery, if for real branching. One-line a && b || c is banned from alerting paths in serious runbooks — the false-page failure mode is well known.
Verdicts are captured, not re-derived. rc=$? on the next line; the specific number is logged alongside the failure message, because "failed with 2" and "failed with 127" route to different runbooks.
Question-commands run in quiet mode. grep -q, and later command -v, cmp -s — when only the verdict matters, output is noise in the logs; and non-zero is always checked for which non-zero when "no" and "couldn't look" need different handling.
CI pipelines are treated as verdict chains. Every step is a script with a trustworthy exit code; the pipeline product's red ✗ is just $? rendered in color. Debugging a red step starts with reproducing its exit code locally, not with reading its logs top to bottom.
E2. Production-practice table
| Symptom | What is really happening | What to run | The fix |
|---|---|---|---|
| CI shows the step green, but the work clearly failed | The script's last command succeeded (often a final echo/cleanup), and with no explicit exit that verdict became the script's — Rule 1 of C2 | ./script.sh ; echo "$?" — reproduce the lying verdict locally | Add guard clauses (step || exit 1) or adopt set -e (Module 14); delete decorative final commands |
| Monitoring says a job "failed with 127" after last night's deploy | Not a logic failure — the command no longer exists on PATH there (removed package, renamed script, cron's minimal PATH) | type -a commandname on the affected host | Restore the binary or fix PATH/the invocation; treat 127/126 as deployment signals, not code bugs |
| The || alert branch pages on-call, but the main command's own log shows success | The a && b || c trap — || fired on b's failure, not a's | Replay with pills: true && false || echo would-page | Rewrite alert paths as explicit if blocks (Module 5) so the else binds to the command you mean |
| A "check if present" automation treats a missing/unreadable file as "feature disabled" | grep's 1 (not found) and 2 (couldn't search) are being lumped together as "non-zero" | grep -q pattern file ; echo "$?" against a deliberately missing file | Capture rc=$? and branch on the specific number; alert on 2 instead of concluding "absent" |
| Debugging notes contradict themselves about what $? was | The evidence evaporated — every inspection command overwrote the board | Re-run the failing command, then rc=$? immediately | Make rc=$? a reflex; in scripts, log rc in the same line as the error message |
E3. 🎓 Capstone — four tickets from the queue
🎓 Ticket 1 — "Our nightly backup job has shown green for three weeks. Today someone needed a restore — the backup directory is empty since the 12th. The script ends with: echo \"backup finished at $(date)\"."
Diagnosis. C2 Rule 1: no explicit exits, and the final echo — which cannot meaningfully fail — became the script's verdict every night. The actual backup command has been failing since the 12th, its non-zero verdict overwritten one line later. Green is the color of the echo.
Work the steps: run the script manually; watch the middle: the backup command prints an error, then echo "$?" right after the script shows 0. Bisect with rc=$? captures after each real step to find the failing one.
Fix and prevention: guard the work: backup_cmd || exit 1 (or set -e, Module 14). Keep the timestamp echo — after the guard. Then fix the monitoring blind spot: a job's success signal must derive from the work's verdict, and ideally from evidence (a freshness check on the newest backup file — Modules 5 and 15 build exactly that).
🎓 Ticket 2 — "One-liner in our runbook: systemctl restart app && echo restarted || systemctl start app. Sometimes after using it, the app is running twice the restart path AND the start path have clearly both executed."
Diagnosis. B2's trap, in the wild. The intended logic was "restart; if that's impossible, start." But || watches the last verdict: if the restart succeeds and the echo somehow fails — or, subtler and commoner, if "restarted" was misdiagnosed and the middle command is actually something fallible — the start branch fires after a successful restart. The three-part one-liner cannot express "else of the first command."
Work the steps: replay with pills to convince the runbook's author: true && false || echo starts-anyway. Then check the actual middle command's failure modes.
Fix and prevention: make it two honest lines, or a real if/else (Module 5): if ! systemctl restart app; then systemctl start app; fi. Runbook policy: &&/|| one-liners may guard, never branch — anything with an else goes through if.
🎓 Ticket 3 — "Compliance automation checks each host for a hardening flag: grep -q 'PermitRootLogin no' /etc/ssh/sshd_config || echo NONCOMPLIANT. The report flagged 40 hosts noncompliant; 12 of them, on inspection, have the flag set and correct."
Diagnosis. C1's phone-call caveat: on those 12 hosts the config lives elsewhere (a drop-in directory, different path) or is unreadable to the automation's user — grep exited 2 ("couldn't look"), the || heard only "non-zero," and "couldn't look" was reported as "not compliant."
Work the steps: on an affected host, run the grep manually and read the number: grep -q 'PermitRootLogin no' /etc/ssh/sshd_config ; echo "$?" → 2, plus grep's stderr message naming the real problem (missing file / permission denied).
Fix and prevention: branch on the specific code — rc=$?; 1 → truly noncompliant; 2 → "audit error: could not read config," a different queue entirely. General law for auditing automation: absence of evidence (1) and absence of access (2) must never share a bucket.
🎓 Ticket 4 — "A deploy pipeline step runs ./migrate.sh; the step went red with exit code 126 on exactly half the fleet, 0 on the rest. Same repo revision everywhere."
Diagnosis. 126 = found but not executable (A2). Same revision, different arrival: half the hosts received the file through a path that dropped the execute bit (Module 1's E1 warned that the bit ships with the repo — an rsync flag, an unpack step, or a checkout on a filesystem that ignores permissions can strip it).
Work the steps: on a red host: ls -l migrate.sh → no x. Compare with a green host. Then find the distribution difference (deploy tooling logs, rsync options, archive format).
Fix and prevention: immediate: chmod +x via the deploy tool and re-run. Durable: commit the execute bit (git update-index --chmod=+x where needed) and add a preflight guard to the pipeline that fails with a clear message before the run step: a one-line executability check turns tomorrow's mysterious 126 into "migrate.sh not executable on host X." The number told you the whole story before you opened a single log — that is the point of this module.
E4. Documentation reference
| Topic | Authoritative reference | What you'll find there |
|---|---|---|
| Exit status rules | Bash manual — Exit Status | The 0-255 range, and the reserved 126 / 127 / 128+N meanings |
| ;, &&, || | Bash manual — Lists of Commands | Exact semantics and (flat) precedence of the command-list operators |
| exit (and source caveat) | Bash manual — Bourne Shell Builtins | What exit does, and its behavior in sourced files |
| A model exit-code vocabulary | grep(1) — man7.org | The EXIT STATUS section: 0 found / 1 not found / 2 error — codes as meaning |
| The two pills | true(1) — man7.org · false(1) — man7.org | The success and failure test commands used throughout this module |
E5. Self-assessment
Answer from memory, out loud or on paper. Every answer is in this module.
- What is an exit status, who produces it, and where does bash keep the most recent one?
- Why does 0 mean success — what is the design logic behind giving failure all the other numbers?
- What do 126 and 127 mean, which Module-1 error message pairs with each, and why does the difference matter to monitoring?
- a ; b vs a && b vs a || b — one sentence each on when b runs.
- Why is a && b || c not if/else? Give the two-pill one-liner that proves it.
- What is the exit status of the whole line false && echo hi, and why is that useful?
- What does grep -q change about grep, and what do its exit codes 0, 1, and 2 each mean?
- Why must "not found" (1) and "couldn't search" (2) sometimes be handled differently? Give a real scenario.
- A script has no exit statements. What determines its exit status, and what bug does a final echo "done" invite?
- Write the one-line guard that stops a script with status 1 when a required word is missing from a file.
- Why is $? called perishable, and what is the one-line habit that fixes it?
- After rc=$? runs, what is $? — and why is that fine?
E6. Sources
LinuxTeck — Debugging Shell Scripts & Exit Status Explained (Part 5 of 34) — verbatim FAQ questions used: "What does exit code 127 mean in bash?", "What does exit code 126 mean and how do I fix it?", "How do I check the exit status of the last command in Linux?" (published April 29, 2026)
Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "What is the difference between && and || in Bash?" (published June 18, 2026)
Edureka — Top 60 Shell Scripting Interview Questions and Answers — "How to check if the previous command was run successfully?" (page updated Dec 9, 2024)
A note on the corpus: exit codes are well covered as interview material, but surprisingly few sites publish verbatim exit-code question lists — much of the corpus is tutorials with FAQ headings. Where a question's wording in this module has no published source, its summary line says so plainly instead of inventing an attribution. LinuxTeck's FAQ topics on set -e, pipefail, and PIPESTATUS belong to later modules of this track and are treated there. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2; dates and machine-dependent values are flagged where they occur.